#include "stdafx.h"

#include "StickyTweetsXP.h"
#include "TwitterContentHandler.h"
#include "AtomContentHandler.h"

#include "StatusEdit.h"
#include "SearchEdit.h"
#include "ChildControl.h"

#include "ScrollData.h"

#include "AboutDialog.h"
#include "LoginDialog.h"
#include "SettingsDialog.h"
#include "SwitchUserDialog.h"

#include "Sandbox.h"

// Global Variables:
HINSTANCE hInst;
HWND hWndStickyTweets;
HWND hWndRefresh;
HWND hWndSettings;
HWND hWndSwitchUser;
HWND hWndUpgrade;
HWND hWndError;
HWND hWndCharCounter;
HWND hWndCancel;
HWND hWndStatus;
HWND hWndUpdate;
HWND hWndSearchEdit;
HWND hWndSearchButton;
HWND hWndFollow;
HWND hWndFilter;
HWND hWndTweetsChild;

HBITMAP hSearchBitmap;

TCHAR szTitle[MaxLoadString];					// The title bar text
TCHAR szWindowClass[MaxLoadString];			// the main window class name
TCHAR szTweetsChildClass[MaxLoadString];  // Tweets child window class

NOTIFYICONDATA notifyIcon; //< Our notify icon
HMENU hTaskbarPopup(NULL);
HCURSOR hDefaultCursor(NULL);
HCURSOR hHandCursor(NULL);

CTime thisPubDate(2010, 4, 12, 0, 0, 0);

TwitterUIText homeLink = {CString(L"Home"), RectF()};
TwitterUIText directLink = {CString(L"Direct"), RectF()};
TwitterUIText userTimelineLink;
TwitterUIText searchLink;
BOOL firstCheck(TRUE);
BOOL requireRefresh[NumPages] = {FALSE, FALSE, FALSE, FALSE};
int newTweetTotal(0);
bool upgrade(false);
StickyTweetsPage stPage(CurrentTimeline);
CAtlList<TwitterStatus> homeUI; //< This one stores an ever-increasing list for the Home tab
CAtlList<TwitterStatus> directUI; //< The Direct tab messages
CAtlList<TwitterStatus> userUI; //< The User tab tweets
CAtlList<TwitterStatus> searchUI; //< The Search tab tweets
CAtlList<TwitterStatus>* pCurrentTweets(NULL); //< A pointer to the current UI
CAtlMap<CString, Image*> imageMap;
CString inReplyToId; //< ID of tweet user is replying too
CString lastViewedId; //< ID of most recently 'viewed' tweet

RectF searchEditRect;
RectF userIconRect; //< On the user page, the rect for the big icon
ScrollData scrollData;
INT scrollPositions[NumPages] = {0, 0, 0, 0}; //< Positions for each tab

const INT MinWindowWidth = 448;
const INT MinWindowHeight = 176;

const REAL LineHeight = 18.0f;
const REAL MinVerticalSpacing = 54.0f;
const INT TweetsChildYNormal = 91;
const INT TweetsChildYUserNormal = 160;
const INT TweetsChildXNormal = 9;

#define FollowButtonXNormal   64
#define FollowButtonYNormal   96

#define FilterButtonXNormal   168
#define FilterButtonYNormal   96

SolidBrush* pBrush;
SolidBrush* pBrushBlue;
SolidBrush* pBrushDate;
Font* pFontHeader;
Font* pFontLink;
Font* pFont;
Font* pFontScreenName;
Font* pFontPageTitle;
Font* pFontDate;

Image* pReplyImage(NULL);
Image* pRetweetImage(NULL);
Image* pSeasonal(NULL);

HANDLE hCancelBitmap(NULL);

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);

 	int exitCode = 0;
	MSG msg;
	HACCEL hAccelTable;

  hInst = hInstance; // Store instance handle in our global variable
  pCurrentTweets = &homeUI;

	// Initialize global strings
	LoadString(hInstance, IDS_APP_TITLE, szTitle, MaxLoadString);
	LoadString(hInstance, IDC_STICKYTWEETSXP, szWindowClass, MaxLoadString);
	LoadString(hInstance, IDC_TWEETSCHILD, szTweetsChildClass, MaxLoadString);

  hSearchBitmap = LoadBitmap(hInstance, MAKEINTRESOURCE(IDB_SEARCH));

	// Initialize COM
	CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);

  // Initialize GDI+
  ULONG_PTR gdiToken;
  GdiplusStartupInput gdiInput;
  
  Gdiplus::GdiplusStartup(&gdiToken, &gdiInput, NULL);

  // Load settings
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus error = app.LoadSettings();
  Settings settings = app.GetSettings();

  app.OpenMainContext(AsyncCallback);

#ifdef _DEBUG
  //Sandbox sandbox;
  //sandbox.PrintVersion();
#endif

  // Login
  if (settings.Password.GetLength() == 0)
  {
    error = (StickyTweetsStatus)DialogBox(hInstance, MAKEINTRESOURCE(IDD_LOGIN), NULL, LoginDialog);
  }
  else
  {
    StickyTweetsStatus verifyStatus = app.VerifyCredentials(settings.ScreenName, settings.Password);
    switch (verifyStatus)
    {
    case ErrorInternetConnection:
      nCmdShow = SW_HIDE;
      error = StatusStOk;
      break;

    case ErrorTwitterVerify:
      error = (StickyTweetsStatus)DialogBox(hInstance, MAKEINTRESOURCE(IDD_LOGIN), NULL, LoginDialog);
      break;

    default:
      error = verifyStatus;
      break;
    }
  }

  if (error == StatusStOk)
  {
    // Pre-create the brushes and fonts for drawing
    SolidBrush brush(Color(0x2, 0x2, 0x2));
    pBrush = &brush;
    SolidBrush brushBlue(Color(0x8, 0x8, 0xf7));
    pBrushBlue = &brushBlue;
    SolidBrush brushDate(Color(0x99, 0x99, 0xac));
    pBrushDate = &brushDate;
    FontFamily arial(L"Arial");
    FontFamily* fontFamily(&arial);
    Font fontHeader(fontFamily, 14);
    pFontHeader = &fontHeader;
    Font fontLink(fontFamily, 16, FontStyleBold, UnitPixel);
    pFontLink = &fontLink;
    Font font(fontFamily, 14, FontStyleRegular, UnitPixel);
    pFont = &font;
    Font fontScreenName(fontFamily, 14, FontStyleBold, UnitPixel);
    pFontScreenName = &fontScreenName;
    Font fontPageTitle(fontFamily, 18, FontStyleBold, UnitPixel);
    pFontPageTitle = &fontPageTitle;
    Font fontDate(fontFamily, 12, FontStyleItalic, UnitPixel);
    pFontDate = &fontDate;

    pReplyImage = Bitmap::FromResource(hInstance, MAKEINTRESOURCE(IDB_REPLY));
    pRetweetImage = Bitmap::FromResource(hInstance, MAKEINTRESOURCE(IDB_RETWEET));
    hCancelBitmap = LoadImage(hInstance, MAKEINTRESOURCE(IDB_CANCEL), IMAGE_BITMAP, 0, 0, LR_LOADTRANSPARENT);
    
    // Register window class
	  WNDCLASSEX wcex;
	  wcex.cbSize = sizeof(WNDCLASSEX);
	  wcex.style			= 0;
	  wcex.lpfnWndProc	= StickyTweetsWindow;
	  wcex.cbClsExtra		= 0;
	  wcex.cbWndExtra		= 0;
	  wcex.hInstance		= hInstance;
	  wcex.hIcon			= LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TWITTER));
	  wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	  wcex.hbrBackground	= CreateSolidBrush(RGB(251, 251, 251));// (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName	= L"";
	  wcex.lpszClassName	= szWindowClass;
	  wcex.hIconSm		= LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_TWITTER));

    RegisterClassEx(&wcex);

	  // Perform application initialization:
	  if (!InitInstance (hInstance, nCmdShow))
	  {
		  return FALSE;
	  }

	  hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_STICKYTWEETSXP));
	  // Main message loop:
	  while (GetMessage(&msg, NULL, 0, 0))
	  {
		  if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
		  {
			  TranslateMessage(&msg);
			  DispatchMessage(&msg);
		  }
	  }

    BOOL notifyOk = Shell_NotifyIcon(NIM_DELETE, &notifyIcon);

	  exitCode = (int)msg.wParam;
  }

  // Clean up GDI+
  GdiplusShutdown(gdiToken);

  // Clean up COM
  CoUninitialize();

  return exitCode;
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hWndStickyTweets = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

   if (!hWndStickyTweets)
   {
      return FALSE;
   }

   ShowWindow(hWndStickyTweets, nCmdShow);
   UpdateWindow(hWndStickyTweets);

   return TRUE;
}

LRESULT CALLBACK StickyTweetsWindow(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	int wmId, wmEvent;
	PAINTSTRUCT ps;
	HDC hdc;

	switch (message)
	{
  case WM_CREATE:
    StickyTweetsWindow_Create(hWnd);
    break;

  case WM_GETMINMAXINFO:
    {
      LPMINMAXINFO minMaxInfo = (LPMINMAXINFO)lParam;
      minMaxInfo->ptMinTrackSize.x = MinWindowWidth;
      minMaxInfo->ptMinTrackSize.y = MinWindowHeight;
    }
    break;

  case WM_SIZE:
    StickyTweetsWindow_Size(LOWORD(lParam), HIWORD(lParam));
    break;

  case WM_SETFOCUS:
    SetFocus(hWndStatus);
    break;

  case WM_ACTIVATE:
    if (LOWORD(wParam) == WA_INACTIVE)
      UpdateLastViewed();
    break;

  case WM_CTLCOLORSTATIC:
    SetBkColor((HDC)wParam, RGB(255, 255, 255));
    break;

	case WM_PAINT:
		hdc = BeginPaint(hWnd, &ps);
		StickyTweetsWindow_Paint(hdc, ps);
		EndPaint(hWnd, &ps);
		break;

  case WM_PRINTCLIENT:
    hdc = (HDC)wParam;
    ps.hdc = hdc;
    GetClientRect(hWnd, &ps.rcPaint);
    StickyTweetsWindow_Paint(hdc, ps);
    break;

  case WM_KEYDOWN:
    StickyTweetsWindow_KeyDown((UINT)wParam, (INT)(SHORT)LOWORD(lParam), (UINT)HIWORD(lParam));
    break;

  case WM_MOUSEMOVE:
    StickyTweetsWindow_MouseMove(MAKEPOINTS(lParam));
    break;

  HANDLE_MSG(hWnd, WM_MOUSEWHEEL, StickyTweetsWindow_MouseWheel);

  case WM_LBUTTONDOWN:
    StickyTweetsWindow_LButtonDown(MAKEPOINTS(lParam));
    break;

	case WM_COMMAND:
		wmId    = LOWORD(wParam);
		wmEvent = HIWORD(wParam);
    if (StickyTweetsWindow_Command(hWnd, wmId, wmEvent))
      break;
    else
      return DefWindowProc(hWnd, message, wParam, lParam);

  case WM_CHAR:
    StickyTweetsWindow_Char((UINT)wParam);
    break;

  case WM_NOTIFY:
    StickyTweetsWindow_Notify((LPNMHDR)lParam, (PNMLINK)lParam);
    break;

  case WM_TIMER:
    StickyTweetsWindow_Timer(hWnd, wParam);
    break;

  case WM_CLOSE:
    ShowWindow(hWnd, SW_HIDE);
    UpdateLastViewed();
    break;

	case WM_DESTROY:
		PostQuitMessage(0);
		break;

  case STM_TASKBAR:
    StickyTweetsWindow_Taskbar(hWnd, (UINT)lParam);
    break;

  case STM_ASYNC:
    StickyTweetsWindow_Async((HINTERNET)wParam);
    break;

  case STM_ASYNC_ERR:
    StickyTweetsWindow_Async_Failed((HINTERNET)wParam);
    break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

LRESULT CALLBACK TweetsChild(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	PAINTSTRUCT ps;
	HDC hdc;

	switch (message)
	{
  case WM_ERASEBKGND:
    {
      HDC hdc = (HDC)wParam;
      HBRUSH offWhite = CreateSolidBrush(RGB(251, 251, 251));
      SelectObject(hdc, offWhite);
      RECT clientRect;
      GetClientRect(hWnd, &clientRect);
      Rectangle(hdc, clientRect.left - 1, clientRect.top - 1, clientRect.right + 1, clientRect.bottom + 1);
      DeleteObject(offWhite);
      return TRUE;
    }
    break;

  case WM_PAINT:
	  hdc = BeginPaint(hWnd, &ps);
    if (scrollData.NumLines > 0)
    {
      POINT ptOrgPrev;
      OffsetRect(&ps.rcPaint, 0, scrollData.ScrollBarPos * scrollData.LineHeight);
      GetWindowOrgEx(ps.hdc, &ptOrgPrev);
      SetWindowOrgEx(ps.hdc, ptOrgPrev.x, ptOrgPrev.y + scrollData.ScrollBarPos * scrollData.LineHeight, NULL);

      INT minIdx = max(ps.rcPaint.top / scrollData.LineHeight, 0);
      INT maxIdx = min((ps.rcPaint.bottom + scrollData.LineHeight - 1) / scrollData.LineHeight, scrollData.NumLines);

      // Tweet align (will be removed later when we add multi-line tweet drawing)
      minIdx /= 3;
      if (maxIdx % 3 != 0)
        maxIdx += (3 - (maxIdx % 3));

      Graphics graphics(hdc);
      RectF invalidRect((REAL)ps.rcPaint.left, (REAL)ps.rcPaint.top, (REAL)(ps.rcPaint.right - ps.rcPaint.left), (REAL)(ps.rcPaint.bottom - ps.rcPaint.top));
      TweetsChild_Paint(StickyTweetsAppXP::GetInstance(), graphics, invalidRect, minIdx, maxIdx);
    }
	  EndPaint(hWnd, &ps);
    break;

  case WM_MOUSEMOVE:
    TweetsChild_MouseMove(MAKEPOINTS(lParam));
    break;

  case WM_LBUTTONDOWN:
    TweetsChild_LButtonDown(MAKEPOINTS(lParam));
    break;

  case WM_VSCROLL:
    TweetsChild_VScroll((UINT)(LOWORD(wParam)), (INT)HIWORD(wParam));
    break;

	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

VOID CALLBACK AsyncCallback(HINTERNET hInternet, __in DWORD_PTR pContext, DWORD internetStatus, __in_bcount(dwStatusInformationLength)LPVOID pStatusInformation, DWORD statusInformationLength)
{
  DWORD error(ERROR_SUCCESS);
  DWORD bytes(0);

  MainContext* pMainContext(NULL);
  RequestContext* pRequestContext(NULL);

  if (*((DWORD*)pContext) == IctMain)
  {
    pMainContext = (MainContext*)pContext;
  }
  else
  {
    pRequestContext = (RequestContext*)pContext;
  }

  //ATLTRACE2(L"Callback Received for Handle %p\n", hInternet);

  switch (internetStatus)
  {
    case INTERNET_STATUS_RESPONSE_RECEIVED:
      // Verify we've a valid pointer with the correct size
      if(pStatusInformation && statusInformationLength == sizeof(DWORD))
      {
        bytes = *((LPDWORD)pStatusInformation);
        //ATLTRACE2(L"Status: Response Received (%d bytes)\n", bytes);
      }
      else
      {
        ATLTRACE2(L"Response Received: pStatusInformation not valid\n");
      }
      break;

    case INTERNET_STATUS_REQUEST_COMPLETE:
      //ATLTRACE2(L"Status: Request complete\n");
      
      // Check for error first            
      error = ((LPINTERNET_ASYNC_RESULT)pStatusInformation)->dwError;
          
      if (error != ERROR_SUCCESS)
      {
        PostMessage(hWndStickyTweets, STM_ASYNC_ERR, (WPARAM)pRequestContext->Request, NULL);
        ATLTRACE2(L"Request complete error\n");
        break;
      }
          
      if (!pRequestContext->DownloadedSize)
      {
        EnterCriticalSection(&pRequestContext->CriticalSection);
        {
          pRequestContext->ReceiveDone = TRUE;
        }
        LeaveCriticalSection(&pRequestContext->CriticalSection);
      }
      else if (pRequestContext->DownloadedSize != InvalidDownloadSize)
      {
        pRequestContext->OutputBuffer[pRequestContext->DownloadedSize] = '\0';
        CString textBuffer(pRequestContext->OutputBuffer);
        pRequestContext->OutputXml.Append(textBuffer);
      }
      else
      {
        // pRequestContext->DownloadedSize equals InvalidDownloadSize
        // We're in the initial state of the response's download
        //ATLTRACE2(L"Ready to start reading the response body\n");
      }
      
      while (InternetReadFile(pRequestContext->Request, pRequestContext->OutputBuffer, IoBufferLength - 1, &pRequestContext->DownloadedSize))
      {
        // Completed synchronously; callback won't be issued
        if (pRequestContext->DownloadedSize == 0)
        {
          EnterCriticalSection(&pRequestContext->CriticalSection);
          {
            pRequestContext->ReceiveDone = TRUE;
          }
          LeaveCriticalSection(&pRequestContext->CriticalSection);
          
          PostMessage(hWndStickyTweets, STM_ASYNC, (WPARAM)pRequestContext->Request, NULL);
          break;
        }

        pRequestContext->OutputBuffer[pRequestContext->DownloadedSize] = '\0';
        CString textBuffer(pRequestContext->OutputBuffer);
        pRequestContext->OutputXml.Append(textBuffer);
      }

      if ((error=GetLastError()) == ERROR_IO_PENDING)
      {
        //ATLTRACE2(L"Waiting for InternetReadFile to complete\n");
      }

      break;
  
    case INTERNET_STATUS_REQUEST_SENT:
      // Verify we've a valid pointer with the correct size
      if(pStatusInformation && statusInformationLength == sizeof(DWORD))
      {
        bytes = *((LPDWORD)pStatusInformation);
        //ATLTRACE2(L"Status: Request sent (%d bytes)\n", bytes);
      }
      else
      {
        //ATLTRACE2(L"Request sent: pStatusInformation not valid\n");
      }
      break;

    default:
      break;
  }
}

VOID StickyTweetsWindow_Create(HWND hWnd)
{
  hDefaultCursor = LoadCursor(NULL, IDC_ARROW);
  hHandCursor = LoadCursor(NULL, IDC_HAND);

  LOGFONT statusLogFont = {
    17,
    0,
    0,
    0,
    FW_NORMAL,
    FALSE,
    FALSE,
    FALSE,
    DEFAULT_CHARSET,
    OUT_DEFAULT_PRECIS,
    CLIP_DEFAULT_PRECIS,
    CLEARTYPE_QUALITY,
    FF_DONTCARE | DEFAULT_PITCH,
    L"Arial",
  };

  HFONT statusFont = CreateFontIndirect(&statusLogFont);

  LOGFONT boldLogFont = {
    17,
    0,
    0,
    0,
    FW_BOLD,
    FALSE,
    FALSE,
    FALSE,
    DEFAULT_CHARSET,
    OUT_DEFAULT_PRECIS,
    CLIP_DEFAULT_PRECIS,
    CLEARTYPE_QUALITY,
    FF_DONTCARE | DEFAULT_PITCH,
    L"Arial",
  };

  HFONT boldFont = CreateFontIndirect(&boldLogFont);

  CString controlText;

  // Create a STATIC control for an error message
  hWndError = CreateWindow(L"STATIC",
                           NULL,
                           WS_CHILD | WS_VISIBLE,
                           0, 0, 0, 0,
                           hWnd,
                           NULL,
                           hInst,
                           NULL);

  SendMessage(hWndError, WM_SETFONT, (WPARAM)statusFont, 0);

  // Create a STATIC control for a character counter
  hWndCharCounter = CreateWindow(L"STATIC",
                                 L"140",
                                 WS_CHILD | WS_VISIBLE | SS_RIGHT,
                                 0, 0, 0, 0,
                                 hWnd,
                                 NULL,
                                 hInst,
                                 NULL);

  SendMessage(hWndCharCounter, WM_SETFONT, (WPARAM)statusFont, 0);

  // Create Cancel BUTTON
  hWndCancel = CreateWindow(L"BUTTON",
                            NULL,
                            WS_CHILD | BS_PUSHBUTTON | BS_BITMAP,// | BS_FLAT,
                            80 - 16 - 6, 42, 18, 18,
                            hWnd,
                            (HMENU)IDC_CMD_CANCEL,
                            hInst,
                            NULL);
  SendMessage(hWndCancel, BM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hCancelBitmap);

  // Create an EDIT control
  hWndStatus = CreateWindow(L"EDIT",                // Predefined class
                            NULL,                   // No window title
                            WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT | ES_AUTOHSCROLL,
                            0, 0, 0, 0,             // Set size in WM_SIZE message
                            hWnd,                   // Parent window
                            (HMENU)IDC_TXT_STATUS,  // Edit control ID
                            hInst,
                            NULL);                  // Pointer not needed

  SendMessage(hWndStatus, WM_SETFONT, (WPARAM)statusFont, 0);
  SendMessage(hWndStatus, EM_SETLIMITTEXT, (WPARAM)MaxTwitterStatus - 1, 0);

  SetWindowSubclass(hWndStatus, StatusEdit, 0, 0);

  controlText.LoadStringW(IDS_UPDATE);
  hWndUpdate = CreateWindow(L"BUTTON",
                            controlText,
                            WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON,
                            0, 0, 0, 0,
                            hWnd,
                            (HMENU)IDC_CMD_UPDATE,
                            hInst,
                            NULL);

  SendMessage(hWndUpdate, WM_SETFONT, (WPARAM)statusFont, 0);  
  SetWindowSubclass(hWndUpdate, ChildControl, 0, 0);

  // Create an EDIT control
  hWndSearchEdit = CreateWindow(L"EDIT",
                            NULL,
                            WS_CHILD | WS_VISIBLE | ES_LEFT | ES_AUTOHSCROLL,
                            0, 0, 0, 0,
                            hWnd,
                            (HMENU)IDC_TXT_SEARCH,
                            hInst,
                            NULL);

  SendMessage(hWndSearchEdit, EM_SETLIMITTEXT, (WPARAM)(MaxTwitterStatus - 1), 0);
  SendMessage(hWndSearchEdit, WM_SETFONT, (WPARAM)statusFont, 0);
  controlText.LoadStringW(IDS_SEARCHCUE);
  Edit_SetCueBannerText(hWndSearchEdit, controlText.GetString());
  SetWindowSubclass(hWndSearchEdit, SearchEdit, 0, 0);

  // Create Search BUTTON
  hWndSearchButton = CreateWindow(L"BUTTON",
                            NULL,
                            WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_BITMAP,
                            0, 0, 0, 0,
                            hWnd,
                            (HMENU)IDC_CMD_SEARCH,
                            hInst,
                            NULL);
  SendMessage(hWndSearchButton, BM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hSearchBitmap);
  SetWindowSubclass(hWndSearchButton, ChildControl, 0, 0);

  // Create Refresh SysLink control
  controlText.LoadStringW(IDS_CMD_REFRESH);
  hWndRefresh = CreateWindow(WC_LINK,
                           controlText,
                           WS_CHILD | WS_VISIBLE,
                           0, 0, 0, 0,
                           hWnd,
                           NULL,
                           hInst,
                           NULL);

  SendMessage(hWndRefresh, WM_SETFONT, (WPARAM)boldFont, 0);
  SetWindowSubclass(hWndRefresh, ChildControl, 0, 0);

  // Create Settings SysLink control
  controlText.LoadStringW(IDS_CMD_SETTINGS);
  hWndSettings = CreateWindow(WC_LINK,
                           controlText,
                           WS_CHILD | WS_VISIBLE,
                           0, 0, 0, 0,
                           hWnd,
                           NULL,
                           hInst,
                           NULL);

  SendMessage(hWndSettings, WM_SETFONT, (WPARAM)boldFont, 0);
  SetWindowSubclass(hWndSettings, ChildControl, 0, 0);

  // Create Switch User SysLink control
  controlText.LoadStringW(IDS_CMD_SWITCHUSER);
  hWndSwitchUser = CreateWindow(WC_LINK,
                           controlText,
                           WS_CHILD | WS_VISIBLE,
                           0, 0, 0, 0,
                           hWnd,
                           NULL,
                           hInst,
                           NULL);

  SendMessage(hWndSwitchUser, WM_SETFONT, (WPARAM)boldFont, 0);
  SetWindowSubclass(hWndSwitchUser, ChildControl, 0, 0);

  // Create Upgrade SysLink control
  controlText.LoadStringW(IDS_CMD_UPGRADE);
  hWndUpgrade = CreateWindow(WC_LINK,
                           controlText,
                           WS_CHILD,
                           0, 0, 0, 0,
                           hWnd,
                           NULL,
                           hInst,
                           NULL);

  SendMessage(hWndUpgrade, WM_SETFONT, (WPARAM)boldFont, 0);

  // Create Follow BUTTON
  controlText.LoadStringW(IDS_FOLLOWING);
  hWndFollow = CreateWindow(L"BUTTON",
                            controlText,
                            WS_CHILD | BS_AUTOCHECKBOX,
                            FollowButtonXNormal, FollowButtonYNormal, 96, 23,
                            hWnd,
                            (HMENU)IDC_CMD_FOLLOW,
                            hInst,
                            NULL);

  SendMessage(hWndFollow, WM_SETFONT, (WPARAM)statusFont, 0);

  // Create Filter BUTTON
  controlText.LoadStringW(IDS_FILTERING);
  hWndFilter = CreateWindow(L"BUTTON",
                            controlText,
                            WS_CHILD | BS_AUTOCHECKBOX,
                            FilterButtonXNormal, FilterButtonYNormal, 96, 23,
                            hWnd,
                            (HMENU)IDC_CMD_FILTER,
                            hInst,
                            NULL);

  SendMessage(hWndFilter, WM_SETFONT, (WPARAM)statusFont, 0);

  // Tweets child window
  WNDCLASSEX wcex;
  wcex.cbSize = sizeof(WNDCLASSEX);
  wcex.style			= 0;
  wcex.lpfnWndProc	= TweetsChild;
  wcex.cbClsExtra		= 0;
  wcex.cbWndExtra		= 0;
  wcex.hInstance		= hInst;
  wcex.hIcon			= NULL;
  wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
  wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
  wcex.lpszMenuName	= L"";
  wcex.lpszClassName	= szTweetsChildClass;
  wcex.hIconSm		= NULL;

  RegisterClassEx(&wcex);

  hWndTweetsChild = CreateWindow(szTweetsChildClass,
                                NULL,
                                WS_CHILD | WS_VSCROLL,
                                0, 0, 0, 0,
                                hWnd,
                                NULL,
                                hInst,
                                NULL);

  // Taskbar icon
  notifyIcon.cbSize = sizeof(NOTIFYICONDATA);
  notifyIcon.hWnd = hWnd;
  notifyIcon.uID = 0;
  notifyIcon.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
  notifyIcon.uCallbackMessage = STM_TASKBAR;
  notifyIcon.hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_TWITTER));
  wcscpy_s<128>(notifyIcon.szTip, L"Click to view tweets");
  notifyIcon.uVersion = NOTIFYICON_VERSION;

  BOOL taskbarOk = Shell_NotifyIcon(NIM_ADD, &notifyIcon);
  if (taskbarOk)
    taskbarOk = Shell_NotifyIcon(NIM_SETVERSION, &notifyIcon);

  upgrade = CheckForUpgrade();
  if (upgrade)
    ShowWindow(hWndUpgrade, SW_SHOW);
}

VOID StickyTweetsWindow_Size(WORD width, WORD height)
{
  // 8 |_link_64_| 8 |__?_208__| 8 |_err_*_| 8 |_char_cnt_64_| 8 |_75_| 8
  MoveWindow(hWndError, 272, 13, width - (8 + 64 + 8 + 208 + 8 + 8 + 64 + 8 + 75 + 8), 19, FALSE);
  MoveWindow(hWndCharCounter, width - 64 - 8 - 75 - 8, 13, 64, 19, FALSE);

  // 8 |_link_64_| 8 |__st_*__| 8 |_up_75_| 8
  MoveWindow(hWndStatus, 80, 40, width - (8 + 64 + 8 + 75 + 16), 23, FALSE);
  MoveWindow(hWndUpdate, width - 75 - 8, 40, 75, 23, FALSE);

  MoveLinks(width, height);

  RECT invalidRect = {0, 0, width, static_cast<LONG>(homeLink.Rect.GetBottom()) - 1};
  InvalidateRect(hWndStickyTweets, &invalidRect, TRUE);

  ResizeTweetsChild();
  SetTimer(hWndStickyTweets, IDT_CALC_TWEET_RECTS, 500, (TIMERPROC)NULL);
}

VOID StickyTweetsWindow_Paint(HDC hDC, const PAINTSTRUCT& ps)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  if (firstCheck)
  {
    UINT_PTR hTimer = SetTimer(hWndStickyTweets, IDT_REFRESH_TIMELINE, app.GetSettings().RefreshPeriod, (TIMERPROC)NULL);

    requireRefresh[CurrentTimeline] = TRUE;
    requireRefresh[DirectPage] = TRUE;
    ShowCurrentTimeline();
    firstCheck = FALSE;
  }

  Graphics graphics(hDC);

  RectF invalidRect((REAL)ps.rcPaint.left, (REAL)ps.rcPaint.top, (REAL)(ps.rcPaint.right - ps.rcPaint.left), (REAL)(ps.rcPaint.bottom - ps.rcPaint.top));
  RectF intersectRect;

  //ATLTRACE2(L"Paint: { invalidRect: { X: %f, Y: %f, Width: %f, Height: %f } }\n", invalidRect.X, invalidRect.Y, invalidRect.Width, invalidRect.Height);

  if (invalidRect.IsEmptyArea())
    return;

  if (IsSeasonal())
  {
    ImageAttributes imgAttr;
    imgAttr.SetColorKey(Color(255, 0, 0), Color(255, 0, 0), ColorAdjustTypeBitmap);
    graphics.DrawImage(pSeasonal, RectF(0.0f, 0.0f, 64.0f, 64.0f), 0.0f, 0.0f, 64.0f, 64.0f, UnitPixel, &imgAttr);
  }

  // 8 |_link_64_| 8 
  static RectF whatRect;
  if (whatRect.IsEmptyArea())
    graphics.MeasureString(L"What are you doing?", -1, pFontHeader, PointF(80.0, 8.0), &whatRect);
  if (RectF::Intersect(intersectRect, whatRect, invalidRect))
    graphics.DrawString(L"What are you doing?", -1, pFontHeader, PointF(80.0, 8.0), pBrush);

  if (homeLink.Rect.IsEmptyArea())
  {
    graphics.MeasureString(homeLink.Text, -1, pFontLink, PointF(80.0f, 70.0f), NULL, &homeLink.Rect);
    homeLink.Rect.X -= 6.0f;
    homeLink.Rect.Y -= 4.0f;
    homeLink.Rect.Width += 12.0f;
    homeLink.Rect.Height += 4.0f;
  }

  PointF offset(homeLink.Rect.GetRight(), 70.0f);

  if (directLink.Rect.IsEmptyArea())
  {
    graphics.MeasureString(directLink.Text, -1, pFontLink, PointF(offset.X + 6.0f + 8.0f + 6.0f, 70.0f), NULL, &directLink.Rect);
    directLink.Rect.X -= 6.0f;
    directLink.Rect.Y -= 4.0f;
    directLink.Rect.Width += 12.0f;
    directLink.Rect.Height += 4.0f;
  }

  offset.X = directLink.Rect.GetRight();

  if (userTimelineLink.Rect.IsEmptyArea() && userTimelineLink.Text.GetLength() > 0)
  {
    graphics.MeasureString(userTimelineLink.Text, -1, pFontLink, PointF(offset.X + 6.0f + 8.0f + 6.0f, 70.0f), NULL, &userTimelineLink.Rect);
    userTimelineLink.Rect.X -= 6.0f;
    userTimelineLink.Rect.Y -= 4.0f;
    userTimelineLink.Rect.Width += 12.0f;
    userTimelineLink.Rect.Height += 4.0f;
    offset.X = userTimelineLink.Rect.GetRight();
  }

  if (searchLink.Rect.IsEmptyArea() && searchLink.Text.GetLength() > 0)
  {
    graphics.MeasureString(searchLink.Text, -1, pFontLink, PointF(offset.X + 6.0f + 8.0f + 6.0f, 70.0f), NULL, &searchLink.Rect);
    searchLink.Rect.X -= 6.0f;
    searchLink.Rect.Y -= 4.0f;
    searchLink.Rect.Width += 12.0f;
    searchLink.Rect.Height += 4.0f;
  }

  graphics.DrawRectangle(&Pen(Color::LightGray), searchEditRect);

  switch (stPage)
  {
  case CurrentTimeline:
    StickyTweetsWindow_PaintCurrentTimeline(app, graphics, invalidRect);
    break;

  case DirectPage:
    StickyTweetsWindow_PaintDirectPage(app, graphics, invalidRect);
    break;

  case UserTimeline:
    StickyTweetsWindow_PaintUserTimeline(app, graphics, invalidRect);
    break;

  case SearchPage:
    StickyTweetsWindow_PaintSearch(app, graphics, invalidRect);
    break;
  }
}

VOID StickyTweetsWindow_PaintCurrentTimeline(StickyTweetsAppXP& app, Graphics& graphics, const RectF& invalidRect)
{
  RectF intersectRect;
  StringFormat format;

  format.SetAlignment(StringAlignmentCenter);

  if (RectF::Intersect(intersectRect, homeLink.Rect, invalidRect))
    graphics.DrawString(homeLink.Text, -1, pFontLink, homeLink.Rect, &format, pBrush);

  if (RectF::Intersect(intersectRect, directLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), directLink.Rect);
    graphics.DrawString(directLink.Text, -1, pFontLink, directLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, userTimelineLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), userTimelineLink.Rect);
    graphics.DrawString(userTimelineLink.Text, -1, pFontLink, userTimelineLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, searchLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), searchLink.Rect);
    graphics.DrawString(searchLink.Text, -1, pFontLink, searchLink.Rect, &format, pBrush);
  }

  format.SetAlignment(StringAlignmentNear);

  PointF tabPoints[7] = {
    PointF(8.0f, invalidRect.Y + invalidRect.Height),
    PointF(8.0f, homeLink.Rect.GetBottom()),
    PointF(homeLink.Rect.X, homeLink.Rect.GetBottom()),
    PointF(homeLink.Rect.X, homeLink.Rect.Y),
    PointF(homeLink.Rect.GetRight(), homeLink.Rect.Y),
    PointF(homeLink.Rect.GetRight(), homeLink.Rect.GetBottom()),
    PointF(invalidRect.X + invalidRect.Width, homeLink.Rect.GetBottom())
  };

  graphics.DrawLines(&Pen(Color::LightGray), tabPoints, 7);
}

VOID StickyTweetsWindow_PaintDirectPage(StickyTweetsAppXP& app, Graphics& graphics, const RectF& invalidRect)
{
  RectF intersectRect;
  StringFormat format;

  format.SetAlignment(StringAlignmentCenter);

  if (RectF::Intersect(intersectRect, homeLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), homeLink.Rect);
    graphics.DrawString(homeLink.Text, -1, pFontLink, homeLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, directLink.Rect, invalidRect))
    graphics.DrawString(directLink.Text, -1, pFontLink, directLink.Rect, &format, pBrush);

  if (RectF::Intersect(intersectRect, userTimelineLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), userTimelineLink.Rect);
    graphics.DrawString(userTimelineLink.Text, -1, pFontLink, userTimelineLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, searchLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), searchLink.Rect);
    graphics.DrawString(searchLink.Text, -1, pFontLink, searchLink.Rect, &format, pBrush);
  }

  format.SetAlignment(StringAlignmentNear);

  PointF tabPoints[7] = {
    PointF(8.0f, invalidRect.Y + invalidRect.Height),
    PointF(8.0f, directLink.Rect.GetBottom()),
    PointF(directLink.Rect.X, directLink.Rect.GetBottom()),
    PointF(directLink.Rect.X, directLink.Rect.Y),
    PointF(directLink.Rect.GetRight(), directLink.Rect.Y),
    PointF(directLink.Rect.GetRight(), directLink.Rect.GetBottom()),
    PointF(invalidRect.X + invalidRect.Width, directLink.Rect.GetBottom())
  };

  graphics.DrawLines(&Pen(Color::LightGray), tabPoints, 7);
}

VOID StickyTweetsWindow_PaintUserTimeline(StickyTweetsAppXP& app, Graphics& graphics, const RectF& invalidRect)
{
  Settings settings = app.GetSettings();
  RectF intersectRect;
  StringFormat format;

  format.SetAlignment(StringAlignmentCenter);

  if (RectF::Intersect(intersectRect, homeLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), homeLink.Rect);
    graphics.DrawString(homeLink.Text, -1, pFontLink, homeLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, directLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), directLink.Rect);
    graphics.DrawString(directLink.Text, -1, pFontLink, directLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, userTimelineLink.Rect, invalidRect))
    graphics.DrawString(userTimelineLink.Text, -1, pFontLink, userTimelineLink.Rect, &format, pBrush);

  if (RectF::Intersect(intersectRect, searchLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), searchLink.Rect);
    graphics.DrawString(searchLink.Text, -1, pFontLink, searchLink.Rect, &format, pBrush);
  }

  format.SetAlignment(StringAlignmentNear);

  PointF tabPoints[7] = {
    PointF(8.0f, invalidRect.Y + invalidRect.Height),
    PointF(8.0f, userTimelineLink.Rect.GetBottom()),
    PointF(userTimelineLink.Rect.X, userTimelineLink.Rect.GetBottom()),
    PointF(userTimelineLink.Rect.X, userTimelineLink.Rect.Y),
    PointF(userTimelineLink.Rect.GetRight(), userTimelineLink.Rect.Y),
    PointF(userTimelineLink.Rect.GetRight(), userTimelineLink.Rect.GetBottom()),
    PointF(invalidRect.X + invalidRect.Width, userTimelineLink.Rect.GetBottom())
  };

  graphics.DrawLines(&Pen(Color::LightGray), tabPoints, 7);

  if (pCurrentTweets->GetCount() > 0)
  {
    TwitterStatus& twitterStatus = pCurrentTweets->GetHead();
    PointF offset(16.0f, 96.0f); //static_cast<REAL>(TweetsChildYUserNormal - 23));

    if (!settings.NoImages)
    {
      const CAtlMap<CString, Image*>::CPair* image = imageMap.Lookup(twitterStatus.User.ProfileImageUrl.Text);
      if (image != NULL)
      {
        if (userIconRect.IsEmptyArea())
        {
          userIconRect = RectF(offset.X, offset.Y, (REAL)image->m_value->GetWidth(), (REAL)image->m_value->GetHeight());

          RECT followRect;
          GetClientRect(hWndFollow, &followRect);
          MoveWindow(hWndFollow, static_cast<INT>(userIconRect.GetRight()) + 8, static_cast<INT>(userIconRect.GetBottom()) - 48, followRect.right - followRect.left, followRect.bottom - followRect.top, TRUE);

          ResizeTweetsChild();
        }

        if (RectF::Intersect(intersectRect, userIconRect, invalidRect))
        {
          graphics.DrawImage(image->m_value, userIconRect);
        }

        offset.Y = max(offset.Y + userIconRect.Height - 24.0f, offset.Y);
        offset.X += userIconRect.Width;
      }
    }
    else
    {
      offset.X = 64.0f;
      offset.Y = static_cast<REAL>(FollowButtonYNormal + 24);
    }

    graphics.DrawString(twitterStatus.User.ScreenName.Text, -1, pFontPageTitle, offset, NULL, pBrush);
  }
}

VOID StickyTweetsWindow_PaintSearch(StickyTweetsAppXP& app, Graphics& graphics, const RectF& invalidRect)
{
  RectF intersectRect;
  StringFormat format;

  format.SetAlignment(StringAlignmentCenter);

  if (RectF::Intersect(intersectRect, homeLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), homeLink.Rect);
    graphics.DrawString(homeLink.Text, -1, pFontLink, homeLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, directLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), directLink.Rect);
    graphics.DrawString(directLink.Text, -1, pFontLink, directLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, userTimelineLink.Rect, invalidRect))
  {
    graphics.FillRectangle(&SolidBrush(Color::LightGray), intersectRect);
    graphics.DrawRectangle(&Pen(pBrush), userTimelineLink.Rect);
    graphics.DrawString(userTimelineLink.Text, -1, pFontLink, userTimelineLink.Rect, &format, pBrush);
  }

  if (RectF::Intersect(intersectRect, searchLink.Rect, invalidRect))
    graphics.DrawString(searchLink.Text, -1, pFontLink, searchLink.Rect, &format, pBrush);

  format.SetAlignment(StringAlignmentNear);

  PointF tabPoints[7] = {
    PointF(8.0f, invalidRect.Y + invalidRect.Height),
    PointF(8.0f, searchLink.Rect.GetBottom()),
    PointF(searchLink.Rect.X, searchLink.Rect.GetBottom()),
    PointF(searchLink.Rect.X, searchLink.Rect.Y),
    PointF(searchLink.Rect.GetRight(), searchLink.Rect.Y),
    PointF(searchLink.Rect.GetRight(), searchLink.Rect.GetBottom()),
    PointF(invalidRect.X + invalidRect.Width, searchLink.Rect.GetBottom())
  };

  graphics.DrawLines(&Pen(Color::LightGray), tabPoints, 7);

  if (searchLink.Text.GetLength() > 0)
  {
    PointF offset(64.0f, static_cast<REAL>(FollowButtonYNormal + 24));
    graphics.DrawString(searchLink.Text, -1, pFontPageTitle, offset, NULL, pBrush);
  }
}

VOID StickyTweetsWindow_KeyDown(UINT virtualKey, INT repeat, UINT flags)
{
  switch (virtualKey)
  {
  case VK_UP:         ScrollBy(-repeat); break;
  case VK_DOWN:       ScrollBy(+repeat); break;
  case VK_PRIOR:      ScrollBy(-repeat * scrollData.LinesPerPage); break;
  case VK_NEXT:       ScrollBy(+repeat * scrollData.LinesPerPage); break;
  case VK_HOME:       ScrollTo(0); break;
  case VK_END:        ScrollTo(MAXLONG); break;

  case VK_F5:         RefreshLink_Notify(); break;
  }
}

VOID StickyTweetsWindow_MouseMove(POINTS p)
{
  PointF location((REAL)p.x, (REAL)p.y);
  BOOL foundLink(FALSE);

  if (stPage != CurrentTimeline && homeLink.Rect.Contains(location))
    foundLink = TRUE;

  if (stPage != DirectPage && directLink.Rect.Contains(location))
    foundLink = TRUE;

  if (stPage != UserTimeline && userTimelineLink.Rect.Contains(location))
    foundLink = TRUE;

  if (stPage != SearchPage && searchLink.Rect.Contains(location))
    foundLink = TRUE;

  SetCursor(foundLink ? hHandCursor : hDefaultCursor);
}

LRESULT StickyTweetsWindow_MouseWheel(HWND hWnd, INT xPos, INT yPos, INT delta, UINT fwKeys)
{
  UINT uScroll;
  if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &uScroll, 0))
  {
    uScroll = 3;
  }

  if (uScroll == WHEEL_PAGESCROLL)
  {
    uScroll = scrollData.LinesPerPage;
  }

  if (uScroll == 0)
  {
    return 0;
  }

  delta += scrollData.WheelCarryover;

  INT numLines = delta * (INT)uScroll / WHEEL_DELTA;

  scrollData.WheelCarryover = delta - numLines * WHEEL_DELTA / (INT)uScroll;

  ScrollBy(-numLines);

  return 0;
}

VOID StickyTweetsWindow_LButtonDown(POINTS p)
{
  PointF location((REAL)p.x, (REAL)p.y);
  BOOL foundLink(FALSE);

  if (stPage != CurrentTimeline && homeLink.Rect.Contains(location))
  {
    ShowCurrentTimeline();
    foundLink = TRUE;
  }

  if (stPage != DirectPage && directLink.Rect.Contains(location))
  {
    ShowDirectPage();
    foundLink = TRUE;
  }

  if (stPage != UserTimeline && userTimelineLink.Rect.Contains(location))
  {
    ShowUserTimeline();
    foundLink = TRUE;
  }

  if (stPage != SearchPage && searchLink.Rect.Contains(location))
  {
    ShowSearchPage();
    foundLink = TRUE;
  }
}

BOOL StickyTweetsWindow_Command(HWND hWnd, WORD id, WORD commandEvent)
{
  switch (id)
	{
	case IDM_ABOUT:
		DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, AboutDialog);
		break;

	case IDM_EXIT:
  case IDC_CMD_TASKBAR_EXIT:
		DestroyWindow(hWnd);
		break;

  case IDC_CMD_TASKBAR_SHOW:
    ShowStickyTweets();
    break;

  case IDC_CMD_TASKBAR_TWITTER:
    ShellExecute(NULL, NULL, L"http://twitter.com", NULL, NULL, SW_SHOWNORMAL);
    break;

  case IDC_CMD_TASKBAR_DISABLE:
    {
      StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
      Settings settings = app.GetSettings();
      if (settings.NoBalloons)
        CheckMenuItem(hTaskbarPopup, IDC_CMD_TASKBAR_DISABLE, MF_BYCOMMAND | MF_UNCHECKED);
      else
        CheckMenuItem(hTaskbarPopup, IDC_CMD_TASKBAR_DISABLE, MF_BYCOMMAND | MF_CHECKED);
      settings.NoBalloons = !settings.NoBalloons;
      app.SaveSettings(settings);
    }
    break;

  case IDC_CMD_TASKBAR_UPGRADE:
    if (upgrade = CheckForUpgrade())
    {
      RECT rect;
      GetClientRect(hWnd, &rect);
      MoveLinks(static_cast<WORD>(rect.right), static_cast<WORD>(rect.bottom));
      ShowWindow(hWndUpgrade, SW_SHOW);
      ShowStickyTweets();
    }
    break;

  case IDC_CMD_CANCEL:
    SetWindowText(hWndStatus, L"");
    UpdateStatusControls(true);
    SetFocus(hWndStatus);
    break;

  case IDC_CMD_UPDATE:
    if (commandEvent == BN_CLICKED)
    {
      UpdateStatus(hWnd);
      UpdateStatusControls(false);
    }
    break;

  case IDC_CMD_SEARCH:
    Search();
    break;

  case IDC_CMD_FOLLOW:
    if (commandEvent == BN_CLICKED)
      FollowButton_Click();
    break;

  case IDC_CMD_FILTER:
    if (commandEvent == BN_CLICKED)
      FilterButton_Click();
    break;

	default:
		return FALSE;
	}
  return TRUE;
}

VOID StickyTweetsWindow_Char(UINT virtualKey)
{
  switch (virtualKey)
  {
    case VK_TAB:
      {
        HWND focus = GetFocus();

        if (focus == hWndStatus)
          SetFocus(hWndUpdate);
        else if (focus == hWndUpdate)
          SetFocus(hWndSearchEdit);
        else if (focus == hWndSearchEdit)
          SetFocus(hWndSearchButton);
        else if (focus == hWndSearchButton)
          SetFocus(hWndRefresh);
        else if (focus == hWndRefresh)
          SetFocus(hWndSettings);
        else if (focus == hWndSettings)
          SetFocus(hWndSwitchUser);
        else if (focus == hWndSwitchUser)
          SetFocus(hWndStatus);
      }
      break;

    case VK_ESCAPE:
      ShowWindow(hWndStickyTweets, SW_HIDE);
      break;
  }
}

VOID StickyTweetsWindow_Notify(LPNMHDR header, PNMLINK link)
{
  switch (header->code)
  {
  case NM_CLICK:
    if (wcscmp(link->item.szID, L"cmdRefresh") == 0)
    {
      RefreshLink_Notify();
      break;
    }
    else if (wcscmp(link->item.szID, L"cmdSettings") == 0)
    {
      SettingsLink_Notify();
      break;
    }
    else if (wcscmp(link->item.szID, L"cmdSwitchUser") == 0)
    {
      SwitchUserLink_Notify();
      break;
    }
    else if (wcscmp(link->item.szID, L"cmdUpgrade") == 0)
    {
      CString msg, caption;
      msg.LoadStringW(IDS_UPGRADE_MESSAGE);
      caption.LoadStringW(IDS_UPGRADE_CAPTION);
      if (MessageBox(hWndStickyTweets, msg, caption, MB_YESNO | MB_ICONQUESTION) == IDYES)
      {
        CString link;
        link.LoadStringW(IDS_UPGRADE_LINK);
        ShellExecute(NULL, NULL, link, NULL, NULL, SW_SHOWNORMAL);
        PostQuitMessage(0);
      }
      break;
    }
    break;
  }
}

VOID StickyTweetsWindow_Timer(HWND hWnd, UINT timerId)
{
  switch (timerId)
  {
  case IDT_REFRESH_TIMELINE:
    ATLTRACE2(L"IDT_REFRESH_TIMELINE\n");
    if (stPage == CurrentTimeline)
    {
      RefreshDirectPage();
      requireRefresh[CurrentTimeline] = TRUE;
      ShowCurrentTimeline();
    }
    else if (stPage == DirectPage)
    {
      RefreshCurrentTimeline();
      requireRefresh[DirectPage] = TRUE;
      ShowDirectPage();
    }
    else
    {
      RefreshCurrentTimeline();
      RefreshDirectPage();
    }
    break;

  case IDT_CALC_TWEET_RECTS:
    KillTimer(hWndStickyTweets, IDT_CALC_TWEET_RECTS);
    EmptyRects(*pCurrentTweets);
    InvalidateRect(hWndTweetsChild, NULL, TRUE);
    break;
  }
}

VOID StickyTweetsWindow_Taskbar(HWND hWnd, UINT taskbarMessage)
{
  switch (taskbarMessage)
  {
  case WM_LBUTTONDOWN:
    ShowStickyTweets();
    break;

  case WM_CONTEXTMENU:
    {
      HWND hShell = GetShellWindow();
      HWND hTaskbar = FindWindow(L"Shell_TrayWnd", NULL);
      if (hTaskbar != NULL)
      {
        HWND hNotify = FindWindowEx(hTaskbar, NULL, L"TrayNotifyWnd", NULL);
        if (hNotify != NULL)
        {
          RECT notifyRect;
          if (GetWindowRect(hNotify, &notifyRect))
          {
            if (hTaskbarPopup == NULL)
            {
              hTaskbarPopup = CreatePopupMenu();
              AppendMenu(hTaskbarPopup, MF_STRING, IDC_CMD_TASKBAR_SHOW, L"&Show StickyTweets");
              AppendMenu(hTaskbarPopup, MF_STRING, IDC_CMD_TASKBAR_TWITTER, L"Browse to &twitter.com");
              AppendMenu(hTaskbarPopup, MF_STRING, IDC_CMD_TASKBAR_DISABLE, L"&Don't bother me");
              if (StickyTweetsAppXP::GetInstance().GetSettings().NoBalloons)
                CheckMenuItem(hTaskbarPopup, IDC_CMD_TASKBAR_DISABLE, MF_BYCOMMAND | MF_CHECKED);
              AppendMenu(hTaskbarPopup, MF_STRING, IDC_CMD_TASKBAR_UPGRADE, L"Check for &updates");
              AppendMenu(hTaskbarPopup, MF_STRING, IDC_CMD_TASKBAR_EXIT, L"E&xit");
            }
            TrackPopupMenu(hTaskbarPopup, TPM_LEFTALIGN | TPM_BOTTOMALIGN | TPM_LEFTBUTTON | TPM_VERNEGANIMATION, notifyRect.left, notifyRect.top, 0, hWnd, NULL);
          }
        }
      }
    }
    break;

  case NIN_BALLOONUSERCLICK:
    ShowCurrentTimeline();
    ScrollTo(0);
    ShowStickyTweets();
    break;
  }
}

VOID StickyTweetsWindow_Async(HINTERNET hRequest)
{
  ATLTRACE2(L"New async data for HINTERNET: %p\n", hRequest);
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  RequestContext* pRequest = app.GetRequestContext(hRequest);

  if (!pRequest)
  {
    status = ErrorStUnknown;
  }

  DWORD statusCode;
  if (status == StatusStOk)
  {
    status = app.GetStatusCode(pRequest->Request, &statusCode);
  }

  if (status == StatusStOk && statusCode != HTTP_STATUS_OK)
  {
    if (app.SetTwitterError(pRequest->OutputXml) == StatusStOk)
    {
      status = ErrorTwitterError;
    }
    else
    {
      status = ErrorStExtractTwitterError;
    }
  }

  if (status == StatusStOk && pRequest->OutputXml.GetLength() > 0)
  {
    switch (pRequest->Type)
    {
    case RtCurrentTimeline:
      RefreshCurrentTimelineComplete(pRequest);
      break;

    case RtMentions:
      RefreshMentionsComplete(pRequest);
      break;

    case RtDirect:
      RefreshDirectComplete(pRequest);
      break;

    case RtSentDirect:
      RefreshSentDirectComplete(pRequest);
      break;

    case RtUserTimeline:
      RefreshUserTimelineComplete(pRequest);
      break;

    case RtSearch:
      RefreshSearchComplete(pRequest);
      break;
    }
  }

  app.DeleteRequestContext(hRequest);
  DisplayStStatus(hWndStickyTweets, status);
}

VOID StickyTweetsWindow_Async_Failed(HINTERNET hRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  app.DeleteRequestContext(hRequest);
}

VOID RefreshLink_Notify()
{
  BOOL lastViewedUpdated(FALSE);
  switch (stPage)
  {
  case CurrentTimeline:
    lastViewedUpdated = UpdateLastViewed(FALSE);
    requireRefresh[CurrentTimeline] = TRUE;
    ShowCurrentTimeline();

    SetTimer(hWndStickyTweets, IDT_REFRESH_TIMELINE, StickyTweetsAppXP::GetInstance().GetSettings().RefreshPeriod, (TIMERPROC)NULL);
    break;

  case DirectPage:
    requireRefresh[DirectPage] = TRUE;
    ShowDirectPage();
    break;

  case UserTimeline:
    if (userTimelineLink.Text.GetLength() > 0)
    {
      requireRefresh[UserTimeline] = TRUE;
      ShowUserTimeline();
    }
    break;

  case SearchPage:
    if (searchLink.Text.GetLength() > 0)
    {
      requireRefresh[SearchPage] = TRUE;
      ShowSearchPage();
    }
    break;
  }

}

VOID SettingsLink_Notify()
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  Settings oldSettings = app.GetSettings();

  StickyTweetsStatus status = (StickyTweetsStatus)DialogBox(hInst, MAKEINTRESOURCE(IDD_SETTINGS), hWndStickyTweets, SettingsDialog);

  Settings settings = app.GetSettings();

  if (settings.RefreshPeriod != oldSettings.RefreshPeriod)
  {
    SetTimer(hWndStickyTweets, IDT_REFRESH_TIMELINE, settings.RefreshPeriod, (TIMERPROC)NULL);
  }

  if (status == StatusStSettingsSaveRedraw)
  {
    BOOL noImages = app.GetSettings().NoImages;
    if (settings.NoImages != oldSettings.NoImages)
    {
      if (settings.NoImages)
      {
        DeleteImageMap();
      }
      else
      {
        requireRefresh[CurrentTimeline] = TRUE;
        requireRefresh[DirectPage] = TRUE;
        requireRefresh[UserTimeline] = TRUE;
        requireRefresh[SearchPage] = TRUE;
      }
      RedrawCurrentPage();
    }
  }
  DisplayStStatus(hWndStickyTweets, status);
}

VOID SwitchUserLink_Notify()
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status = (StickyTweetsStatus)DialogBox(hInst, MAKEINTRESOURCE(IDD_SWITCHUSER), hWndStickyTweets, SwitchUserDialog);

  if (status == StatusStSwitchUser || status == StatusStSwitchUserNew)
  {
    requireRefresh[CurrentTimeline] = TRUE;
    homeUI.RemoveAll();
    inReplyToId = L"";
    lastViewedId = L"";

    requireRefresh[DirectPage] = TRUE;
    directUI.RemoveAll();

    if (status == StatusStSwitchUserNew)
    {
      status = (StickyTweetsStatus)DialogBox(hInst, MAKEINTRESOURCE(IDD_LOGIN), NULL, LoginDialog);
    }
    else if (status == StatusStSwitchUser)
    {
      app.SetLastScreenName(switchScreenName);
      status = app.LoadSettings(true);
    }

    if (status == StatusStOk)
    {
      status = app.ResetMainContext();
    }

    if (status == StatusStOk)
    {
      if (stPage == CurrentTimeline || stPage == UserTimeline)
      {
        ScrollTo(0);
      }
      RedrawCurrentPage();
    }
  }
  
  DisplayStStatus(hWndStickyTweets, status);
}

VOID FollowButton_Click()
{
  LRESULT checked = SendMessage(hWndFollow, BM_GETCHECK, 0, 0);
  StickyTweetsStatus status = StickyTweetsAppXP::GetInstance().Follow(userTimelineLink.Text, checked == BST_CHECKED);
  if (status >= ErrorFirst)
  {
    SendMessage(hWndFollow, BM_SETCHECK, (checked == BST_CHECKED ? BST_UNCHECKED : BST_CHECKED), 0L);
  }

  DisplayStStatus(hWndStickyTweets, status);

  requireRefresh[CurrentTimeline] = TRUE;
}

VOID FilterButton_Click()
{
  LRESULT checked = SendMessage(hWndFilter, BM_GETCHECK, 0, 0);
  StickyTweetsStatus status = StickyTweetsAppXP::GetInstance().UpdateSearchFilter(searchLink.Text, checked == BST_CHECKED);
  DisplayStStatus(hWndStickyTweets, status);

  requireRefresh[CurrentTimeline] = requireRefresh[UserTimeline] = TRUE;
}

VOID TweetsChild_Paint(StickyTweetsAppXP& app, Graphics& graphics, const RectF& invalidRect, INT minIdx, INT maxIdx)
{
  RectF intersectRect;
  StringFormat format;
  PointF location(64.0f, 0.0f);
  bool resizeChild(false);

  POSITION e = pCurrentTweets->GetHeadPosition();

  while (e)
  {
    TwitterStatus& twitterStatus = pCurrentTweets->GetNext(e);

    INT lastViewedIdLen = lastViewedId.GetLength();
    if (stPage == CurrentTimeline && lastViewedIdLen > 0 && (lastViewedIdLen < twitterStatus.Id.GetLength() || lastViewedId.Compare(twitterStatus.Id) < 0))
    {
      SolidBrush replyBrush(Color::Beige);
      RECT tweetsChildRect;
      GetClientRect(hWndTweetsChild, &tweetsChildRect);
      graphics.FillRectangle(&replyBrush, location.X, location.Y, static_cast<REAL>(tweetsChildRect.right), LineHeight);
    }

    if (inReplyToId.GetLength() > 0 && twitterStatus.Id == inReplyToId)
    {
      SolidBrush replyBrush(Color::LightGray);
      RECT tweetsChildRect;
      GetClientRect(hWndTweetsChild, &tweetsChildRect);
      graphics.FillRectangle(&replyBrush, location.X, location.Y, static_cast<REAL>(tweetsChildRect.right), LineHeight);
    }

    PointF offset = location;

    if (!app.GetSettings().NoImages && stPage != UserTimeline)
    {
      if (twitterStatus.User.ProfileImageUrl.Rect.IsEmptyArea())
        twitterStatus.User.ProfileImageUrl.Rect = RectF(24.0f, offset.Y + 2.0f, 32.0f, 32.0f);

      if (RectF::Intersect(intersectRect, twitterStatus.User.ProfileImageUrl.Rect, invalidRect))
      {
        const CAtlMap<CString, Image*>::CPair* image = imageMap.Lookup(twitterStatus.User.ProfileImageUrl.Text);
        if (image != NULL)
          graphics.DrawImage(image->m_value, twitterStatus.User.ProfileImageUrl.Rect);
      }
    }

    if (stPage != UserTimeline)
    {
      if (twitterStatus.User.ScreenName.Rect.IsEmptyArea())
        graphics.MeasureString(twitterStatus.User.ScreenName.Text, -1, pFontScreenName, offset, &twitterStatus.User.ScreenName.Rect);

      if (RectF::Intersect(intersectRect, twitterStatus.User.ScreenName.Rect, invalidRect))
        graphics.DrawString(twitterStatus.User.ScreenName.Text, -1, pFontScreenName, twitterStatus.User.ScreenName.Rect, &format, pBrushBlue);

      offset.X += twitterStatus.User.ScreenName.Rect.Width;
    }

    REAL tweetTextX = offset.X;
    if (!twitterStatus.HasLayout)
    {
      resizeChild = true;

      RECT clientRect;
      GetClientRect(hWndTweetsChild, &clientRect);

      RectF display = RectF(offset, SizeF(((REAL)clientRect.right) - offset.X, (REAL)MAXLONG));
      twitterStatus.NumLines = 0;

      CharacterRange ranges[MaxTweetWords];
      for (int i = 0; i < twitterStatus.NumWords; i++)
      {
        ranges[i] = CharacterRange(twitterStatus.Words[i].First, twitterStatus.Words[i].Length);
      }

      Region regions[MaxTweetWords];
      format.SetFormatFlags(StringFormatFlagsMeasureTrailingSpaces);
      format.SetMeasurableCharacterRanges(twitterStatus.NumWords, ranges);
      Status status = graphics.MeasureCharacterRanges(twitterStatus.FullText, -1, pFont, display, &format, twitterStatus.NumWords, regions);
      for (int i = 0; i < twitterStatus.NumWords; i++)
      {
        regions[i].GetBounds(&twitterStatus.Words[i].Rect, &graphics);
        if (i == 0 || twitterStatus.Words[i].Rect.Y != twitterStatus.Words[i - 1].Rect.Y)
          twitterStatus.NumLines++;
      }

      twitterStatus.HasLayout = true;
    }

    for (int i = 0; i < twitterStatus.NumWords; i++)
    {
      TwitterTweetWord& word = twitterStatus.Words[i];
      Brush* pWordBrush = word.Link == NoLink ? pBrush : pBrushBlue;
      graphics.DrawString(twitterStatus.FullText.Mid(word.First, word.Length), -1, pFont, PointF(word.Rect.X, word.Rect.Y), &format, pWordBrush);
    }

    offset.X = 64.0f;
    offset.Y += (LineHeight * twitterStatus.NumLines);

    if (twitterStatus.ReplyRect.IsEmptyArea())
      twitterStatus.ReplyRect = RectF(offset, SizeF(16.0f, 16.0f));

    if (RectF::Intersect(intersectRect, twitterStatus.ReplyRect, invalidRect))
      graphics.DrawImage(pReplyImage, twitterStatus.ReplyRect);

    offset.X += 16.0f + 4.0f;

    if (twitterStatus.RetweetRect.IsEmptyArea())
      twitterStatus.RetweetRect = RectF(offset, SizeF(16.0f, 16.0f));

    if (RectF::Intersect(intersectRect, twitterStatus.RetweetRect, invalidRect))
      graphics.DrawImage(pRetweetImage, twitterStatus.RetweetRect);

    offset.X += 16.0f + 4.0f;

    if (twitterStatus.CreatedAt.Rect.IsEmptyArea())
      graphics.MeasureString(twitterStatus.CreatedAt.Text, -1, pFontDate, offset, &twitterStatus.CreatedAt.Rect);

    if (RectF::Intersect(intersectRect, twitterStatus.CreatedAt.Rect, invalidRect))
      graphics.DrawString(twitterStatus.CreatedAt.Text, -1, pFontDate, twitterStatus.CreatedAt.Rect, &format, pBrushDate);

    offset.X += twitterStatus.CreatedAt.Rect.Width;

    if (twitterStatus.Source.Text.GetLength() > 0)
    {
      if (twitterStatus.Source.Rect.IsEmptyArea())
        graphics.MeasureString(twitterStatus.Source.Text, -1, pFontDate, offset, &twitterStatus.Source.Rect);

      if (RectF::Intersect(intersectRect, twitterStatus.Source.Rect, invalidRect))
        graphics.DrawString(twitterStatus.Source.Text, -1, pFontDate, twitterStatus.Source.Rect, &format, pBrushDate);

      offset.X += twitterStatus.Source.Rect.Width;
    }

    if (twitterStatus.InReplyToScreenName.Text.GetLength() > 0)
    {
      if (twitterStatus.InReplyToScreenName.Rect.IsEmptyArea())
        graphics.MeasureString(twitterStatus.InReplyToScreenName.Text, -1, pFontDate, offset, &twitterStatus.InReplyToScreenName.Rect);

      if (RectF::Intersect(intersectRect, twitterStatus.InReplyToScreenName.Rect, invalidRect))
        graphics.DrawString(twitterStatus.InReplyToScreenName.Text, -1, pFontDate, twitterStatus.InReplyToScreenName.Rect, &format, pBrushDate);
    }

    location.Y += LineHeight * (twitterStatus.NumLines + 2);
  }

  if (resizeChild)
  {
    ResizeTweetsChild();
  }
}

VOID TweetsChild_MouseMove(POINTS p)
{
  PointF location((REAL)p.x, (REAL)(p.y + scrollData.ScrollBarPos * scrollData.LineHeight));
  BOOL foundLink(FALSE);

  POSITION e = pCurrentTweets->GetHeadPosition();
  while (!foundLink && e)
  {
    TwitterStatus& twitterStatus = pCurrentTweets->GetNext(e);

    for (int i = 0; i < twitterStatus.NumWords; i++)
    {
      if (twitterStatus.Words[i].Link != NoLink && twitterStatus.Words[i].Rect.Contains(location))
      {
        foundLink = TRUE;
        break;
      }
    }

    if (!foundLink  && (stPage == CurrentTimeline || stPage == DirectPage || stPage == SearchPage) && twitterStatus.User.ScreenName.Rect.Contains(location))
      foundLink = TRUE;

    if (!foundLink && twitterStatus.ReplyRect.Contains(location))
      foundLink = TRUE;

    if (!foundLink && twitterStatus.RetweetRect.Contains(location))
      foundLink = TRUE;
  }

  SetCursor(foundLink ? hHandCursor : hDefaultCursor);
}

VOID TweetsChild_LButtonDown(POINTS p)
{
  PointF location((REAL)p.x, (REAL)(p.y + scrollData.ScrollBarPos * scrollData.LineHeight));
  BOOL foundLink(FALSE);

  POSITION e = pCurrentTweets->GetHeadPosition();

  while (!foundLink && e)
  {
    TwitterStatus& twitterStatus = pCurrentTweets->GetNext(e);

    for (int i = 0; i < twitterStatus.NumWords; i++)
    {
      if (twitterStatus.Words[i].Link != NoLink && twitterStatus.Words[i].Rect.Contains(location))
      {
        // TODO: This is a kludge, fix the content handlers
        CString linkText = twitterStatus.FullText.Mid(twitterStatus.Words[i].First, twitterStatus.Words[i].Length);
        if (twitterStatus.Words[i].Link == WebLink)
        {
          ShellExecute(NULL, NULL, linkText, NULL, NULL, SW_SHOWNORMAL);
        }
        else if (twitterStatus.Words[i].Link == UserLink)
        {
          if (RefreshUserTimeline(linkText.Mid(1)) == StatusStNewUserTimeline || stPage != UserTimeline)
            ShowUserTimeline(true);
        }

        foundLink = TRUE;
        break;
      }
    }

    if (!foundLink && (stPage == CurrentTimeline || stPage == DirectPage || stPage == SearchPage) && twitterStatus.User.ScreenName.Rect.Contains(location))
    {
      RefreshUserTimeline(twitterStatus.User.ScreenName.Text);
      ShowUserTimeline();
      foundLink = TRUE;
    }

    if (!foundLink && twitterStatus.ReplyRect.Contains(location))
    {
      inReplyToId = twitterStatus.Id;
      SetWindowText(hWndStatus, CString(L"@") + twitterStatus.User.ScreenName.Text + L" ");
      SetFocus(hWndStatus);
      SendMessage(hWndStatus, EM_SETSEL, twitterStatus.User.ScreenName.Text.GetLength() + 2, -1);
      UpdateStatusControls();
      InvalidateRect(hWndTweetsChild, NULL, TRUE);
      foundLink = TRUE;
    }

    if (!foundLink && twitterStatus.RetweetRect.Contains(location))
    {
      SetWindowText(hWndStatus, CString(L"RT @") + twitterStatus.User.ScreenName.Text + L": " + twitterStatus.FullText);
      SetFocus(hWndStatus);
      SendMessage(hWndStatus, EM_SETSEL, 0, 0);
      UpdateStatusControls();
      foundLink = TRUE;
    }
  }
}

VOID TweetsChild_VScroll(UINT code, INT position)
{
  switch (code)
  {
    case SB_LINEUP:         ScrollBy(-1); break;
    case SB_LINEDOWN:       ScrollBy(+1); break;
    case SB_PAGEUP:         ScrollBy(-scrollData.LinesPerPage); break;
    case SB_PAGEDOWN:       ScrollBy(+scrollData.LinesPerPage); break;
    case SB_THUMBPOSITION:  ScrollTo(GetTrackPosition(hWndTweetsChild, SB_VERT)); break;
    case SB_THUMBTRACK:     ScrollTo(GetTrackPosition(hWndTweetsChild, SB_VERT)); break;
    case SB_TOP:            ScrollTo(0); break;
    case SB_BOTTOM:         ScrollTo(MAXLONG); break;
  }
}

bool CheckForUpgrade()
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  CString response;  
  StickyTweetsStatus status = app.GetWebResponse(L"stickytweets.codeplex.com", L"/Project/ProjectRss.aspx?ProjectRSSFeed=codeplex%3a%2f%2frelease%2fstickytweets", NULL, NULL, response, TRUE);
  if (status == StatusStOk)
  {
    INT startIdx = response.Find(L"<pubDate>") + 9;
    INT endIdx = response.Find(L"</pubDate>", startIdx);
    CString pubDate = response.Mid(startIdx, endIdx - startIdx);
    CTime pubTime = app.ParseDate(pubDate);
    if (pubTime > thisPubDate)
      return true;
  }
  return false;
}

VOID DisplayStStatus(HWND hWnd, StickyTweetsStatus status)
{
  TCHAR buffer[MaxErrorBuffer] = {'\0'};

  SetWindowText(hWndError, L"");

  switch (status)
  {
  case StatusStOk:
  case StatusStNewCurrentTimeline:
  case StatusStNewUserTimeline:
  case StatusStNewSearch:
  case StatusStSwitchUser:
  case StatusStSwitchUserCancel:
  case StatusStSwitchUserNew:
    break;

  case StatusStFollowing:
    LoadString(hInst, IDS_STATUS_FOLLOWING, buffer, MaxErrorBuffer);
    break;

  case StatusStUnFollowing:
    LoadString(hInst, IDS_STATUS_UNFOLLOWING, buffer, MaxErrorBuffer);
    break;

  case StatusStFiltering:
    LoadString(hInst, IDS_STATUS_FILTERING, buffer, MaxErrorBuffer);
    break;

  case StatusStUnFiltering:
    LoadString(hInst, IDS_STATUS_UNFILTERING, buffer, MaxErrorBuffer);
    break;

  case StatusStSettingsSave:
  case StatusStSettingsSaveRedraw:
    LoadString(hInst, IDS_STATUS_SETTINGS_SAVED, buffer, MaxErrorBuffer);
    break;

  case StatusStDmOk:
    LoadString(hInst, IDS_STATUS_ST_DMOK, buffer, MaxErrorBuffer);
    break;

  case StatusStAsync:
    LoadString(hInst, IDS_STATUS_ASYNC, buffer, MaxErrorBuffer);
    break;

  case WarningStTimeline:
    LoadString(hInst, IDS_WARNING_ST_TIMELINE, buffer, MaxErrorBuffer);
    break;

  case WarningStUserTimeline:
    LoadString(hInst, IDS_WARNING_ST_USERTIMELINE, buffer, MaxErrorBuffer);
    break;

  case ErrorRegCreate:
    LoadString(hInst, IDS_ERROR_REG_CREATE, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetConnection:
    LoadString(hInst, IDS_ERROR_INTERNET_CONNECTION, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetOpen:
    LoadString(hInst, IDS_ERROR_INTERNET_OPEN, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetConnect:
    LoadString(hInst, IDS_ERROR_INTERNET_CONNECT, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetOpenRequest:
    LoadString(hInst, IDS_ERROR_INTERNET_OPENREQUEST, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetSendRequest:
    LoadString(hInst, IDS_ERROR_INTERNET_SENDREQUEST, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetQueryStatus:
    LoadString(hInst, IDS_ERROR_INTERNET_QUERYSTATUS, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetRead:
    LoadString(hInst, IDS_ERROR_INTERNET_READ, buffer, MaxErrorBuffer);
    break;

  case ErrorInternetCreateCache:
    LoadString(hInst, IDS_ERROR_INTERNET_CREATECACHE, buffer, MaxErrorBuffer);
    break;

  case ErrorTwitterError:
    LoadString(hInst, IDS_ERROR_TWITTER_ERROR, buffer, MaxErrorBuffer);
    break;

  case ErrorTwitterVerify:
    LoadString(hInst, IDS_ERROR_TWITTER_VERIFY, buffer, MaxErrorBuffer);
    break;

  case ErrorStTwitterXml:
    LoadString(hInst, IDS_ERROR_ST_TWITTERXML, buffer, MaxErrorBuffer);
    break;

  case ErrorStEmptyStatus:
    LoadString(hInst, IDS_ERROR_ST_EMPTYSTATUS, buffer, MaxErrorBuffer);
    break;

  case ErrorStDmUser:
    LoadString(hInst, IDS_ERROR_ST_DMUSER, buffer, MaxErrorBuffer);
    break;

  default:
    LoadString(hInst, IDS_ERROR_ST_UNKNOWN, buffer, MaxErrorBuffer);
    break;
  }

  if (status >= ErrorTwitterFirst && status < ErrorTwitterMax)
  {
    CString twitterError(L"From twitter: ");
    twitterError.Append(StickyTweetsAppXP::GetInstance().GetTwitterError());
    SetWindowText(hWndError, twitterError);
  }
  else
  {
    SetWindowText(hWndError, buffer);
  }
}

VOID EmptyRects(CAtlList<TwitterStatus>& tweetList)
{
  POSITION e = tweetList.GetHeadPosition();
  while (e)
  {
    TwitterStatus& twitterStatus = tweetList.GetNext(e);
    twitterStatus.EmptyRects();
  }
}

VOID FilterTimeline(CAtlList<TwitterStatus>& tweetList)
{
  ATLTRACE2(L"FilterTimeline\n");
  CAtlList<CString> filters;
  StickyTweetsAppXP::GetInstance().GetSearchFilterList(filters);

  if (filters.GetCount() > 0)
  {
    CAtlList<POSITION> killPositions;

    POSITION e = tweetList.GetHeadPosition();
    while (e)
    {
      TwitterStatus& twitterStatus = tweetList.GetNext(e);

      POSITION filterEnum = filters.GetHeadPosition();
      while (filterEnum)
      {
        wregex filterFinder(filters.GetNext(filterEnum), regex_constants::icase);

        if (regex_search(twitterStatus.FullText.GetString(), filterFinder) || regex_search(twitterStatus.Source.Text.GetString(), filterFinder))
        {
          ATLTRACE2(L" kill tweet: %s\n", twitterStatus.FullText);
          POSITION killPos = e;
          if (e)
            tweetList.GetPrev(killPos);
          else
            killPos = tweetList.GetTailPosition();
          tweetList.RemoveAt(killPos);
          e = tweetList.GetHeadPosition();
          break;
        }
      }
    }
  }
}

CString GetRecipient()
{
  CString result;
  TCHAR status[MaxTwitterStatus];

  int statusLength = GetWindowText(hWndStatus, status, MaxTwitterStatus);
  if (statusLength > 0 && status[0] == L'@')
  {
    for (int i = 1; i < statusLength && status[i] != L' '; i++)
    {
      result.AppendChar(status[i]);
    }
  }

  return result;
}

UINT GetTrackPosition(HWND hWnd, INT scrollBar)
{
  SCROLLINFO si;
  si.cbSize = sizeof(si);
  si.fMask = SIF_TRACKPOS;
  if (GetScrollInfo(hWnd, scrollBar, &si)) {
    return si.nTrackPos;
  }
  return 0;
}

void DeleteImageMap()
{
  POSITION e = imageMap.GetStartPosition();

  while (e)
  {
    CAtlMap<CString, Image*>::CPair* imagePair = imageMap.GetNext(e);
    ATLTRACE2(L"Deleting image: %s\n", imagePair->m_key);
    delete imagePair->m_value;
  }

  imageMap.RemoveAll();
}

bool IsSeasonal()
{
  CTime today = CTime::GetCurrentTime();

  if (today.GetMonth() == 10 && today.GetDay() >= 30)
  {
    if (pSeasonal == NULL)
      pSeasonal = Bitmap::FromResource(hInst, MAKEINTRESOURCE(IDB_GHOST));
    return true;
  }

  if (pSeasonal != NULL)
  {
    delete pSeasonal;
    pSeasonal = NULL;
  }

  return false;
}

void MoveLinks(WORD width, WORD height)
{
  int upgradeOffset = (upgrade ? 75 + 8 : 0);

  // 80 |_h_u_s_| |_*_| |_o._160_| |_o._26_| 16 |_ref_56_| 16 |_set_64_| 8 |_sw_92_| 8 |_up_75_| 8
  searchEditRect = RectF((REAL)(width - 160 - 26 - 16 - 56 - 16 - 64 - 8 - 92 - 8 - upgradeOffset), 67.0, 159.0, 20.0);
  MoveWindow(hWndSearchEdit, (INT)searchEditRect.X + 1, 68, 158, 19, FALSE);
  MoveWindow(hWndSearchButton, width - 26 - 16 - 56 - 16 - 64 - 8 - 92 - 8 - upgradeOffset, 66, 26, 22, FALSE);
  MoveWindow(hWndRefresh, width - 56 - 16 - 64 - 8 - 92 - 8 - upgradeOffset, 70, 56, 19, FALSE);
  MoveWindow(hWndSettings, width - 64 - 8 - 92 - 8 - upgradeOffset, 70, 64, 19, FALSE);
  MoveWindow(hWndSwitchUser, width - 92 - 8 - upgradeOffset, 70, 92, 19, FALSE);
  MoveWindow(hWndUpgrade, width - upgradeOffset, 70, 75, 19, FALSE);
}

void PruneImageMap()
{
  POSITION e = imageMap.GetStartPosition();

  while (e)
  {
    bool prune(true);
    CAtlMap<CString, Image*>::CPair* imagePair = imageMap.GetNext(e);

    POSITION statusE = homeUI.GetHeadPosition();
    while (statusE)
    {
      TwitterStatus& status = homeUI.GetNext(statusE);
      if (status.User.ProfileImageUrl.Text == imagePair->m_key)
      {
        prune = false;
        break;
      }
    }

    if (prune)
    {
      statusE = directUI.GetHeadPosition();
      while (statusE)
      {
        TwitterStatus& status = directUI.GetNext(statusE);
        if (status.User.ProfileImageUrl.Text == imagePair->m_key)
        {
          prune = false;
          break;
        }
      }
    }

    if (prune && userUI.GetCount() > 0)
    {
      TwitterStatus& status = userUI.GetHead();
      if (status.User.ProfileImageUrl.Text == imagePair->m_key)
        continue;
    }

    if (prune)
    {
      statusE = searchUI.GetHeadPosition();
      while (statusE)
      {
        TwitterStatus& status = searchUI.GetNext(statusE);
        if (status.User.ProfileImageUrl.Text == imagePair->m_key)
        {
          prune = false;
          break;
        }
      }
    }

    if (prune)
    {
      ATLTRACE2(L"Deleting image: %s\n", imagePair->m_key);
      delete imagePair->m_value;
      imageMap.RemoveKey(imagePair->m_key);
    }
  }
}

VOID RedrawCurrentPage()
{
  switch (stPage)
  {
  case CurrentTimeline:
    ShowCurrentTimeline(true);
    break;

  case DirectPage:
    ShowDirectPage(true);
    break;

  case UserTimeline:
    ShowUserTimeline(true);
    break;

  case SearchPage:
    ShowSearchPage(true);
    break;
  }
}

StickyTweetsStatus RefreshCurrentTimeline()
{
  ATLTRACE2(L"RefreshCurrentTimeline\n");

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  CString headId;

  if (homeUI.GetCount() > 0 && !requireRefresh[CurrentTimeline])
  {
    headId = homeUI.GetHead().Id;
  }

  CString responseXml, mentionsXml;
  StickyTweetsStatus error = app.GetCurrentTimelineXml(responseXml, headId);

  if (error < ErrorFirst)
  {
    error = app.GetMentions(responseXml, headId);
  }

  DisplayStStatus(hWndStickyTweets, error);
  return error;
}

StickyTweetsStatus RefreshCurrentTimelineComplete(RequestContext* pRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  EmptyRects(homeUI);
  int newTweets = UpdateTimelineUI(pRequest->OutputXml, homeUI);
  //newTweets += UpdateTimelineUI(mentionsXml, homeUI);
  newTweetTotal += newTweets;

  if (newTweets > 0)
  {
    if (stPage == CurrentTimeline)
      RedrawCurrentPage();

    status = StatusStNewCurrentTimeline;

    if (!app.GetSettings().NoBalloons && GetForegroundWindow() != hWndStickyTweets && !firstCheck)
    {
      notifyIcon.uFlags |= NIF_INFO;
      notifyIcon.uTimeout = 10000;
      if (newTweetTotal == 1)
      {
        wcscpy_s<64>(notifyIcon.szInfoTitle, L"1 new tweet!");
        wcscpy_s<256>(notifyIcon.szInfo, L"Let me show you it!");
      }
      else
      {
        CString title;
        title.Format(L"%d new tweets!", newTweetTotal);
        wcscpy_s<64>(notifyIcon.szInfoTitle, title.GetString());
        wcscpy_s<256>(notifyIcon.szInfo, L"Let me show you them!");
      }
      notifyIcon.dwInfoFlags = NIIF_USER;
      Shell_NotifyIcon(NIM_MODIFY, &notifyIcon);
    }
  }

  DisplayStStatus(hWndStickyTweets, status);
  return status;
}

StickyTweetsStatus RefreshMentionsComplete(RequestContext* pRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  EmptyRects(homeUI);

  int newMentions = UpdateTimelineUI(pRequest->OutputXml, homeUI);

  if (newMentions > 0)
  {
    if (stPage == CurrentTimeline)
    {
      RedrawCurrentPage();
    }
  }

  DisplayStStatus(hWndStickyTweets, status);
  return status;
}

StickyTweetsStatus RefreshDirectPage()
{
  ATLTRACE2(L"RefreshDirectPage\n");

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  CString headId;

  if (directUI.GetCount() > 0 && !requireRefresh[DirectPage])
  {
    headId = directUI.GetHead().Id;
  }

  CString responseXml, sentDirectXml;
  StickyTweetsStatus error = app.GetDirectXml(responseXml, headId);

  if (error < ErrorFirst)
  {
    error = app.GetSentDirectXml(responseXml, headId);
  }

  DisplayStStatus(hWndStickyTweets, error);
  return error;
}

StickyTweetsStatus RefreshDirectComplete(RequestContext* pRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  EmptyRects(directUI);
  int newDms = UpdateTimelineUI(pRequest->OutputXml, directUI);
  //newDms += UpdateTimelineUI(sentDirectXml, directUI);

  if (newDms > 0)
  {
    if (stPage == DirectPage)
      RedrawCurrentPage();

    // TODO: Direct
    //error = StatusStNewCurrentTimeline;
    //
    //if (!app.GetSettings().NoBalloons && GetForegroundWindow() != hWndStickyTweets && !firstCheck)
    //{
    //  notifyIcon.uFlags |= NIF_INFO;
    //  notifyIcon.uTimeout = 10000;
    //  if (newTweetTotal == 1)
    //  {
    //    wcscpy_s<64>(notifyIcon.szInfoTitle, L"1 new tweet!");
    //    wcscpy_s<256>(notifyIcon.szInfo, L"Let me show you it!");
    //  }
    //  else
    //  {
    //    CString title;
    //    title.Format(L"%d new tweets!", newTweetTotal);
    //    wcscpy_s<64>(notifyIcon.szInfoTitle, title.GetString());
    //    wcscpy_s<256>(notifyIcon.szInfo, L"Let me show you them!");
    //  }
    //  notifyIcon.dwInfoFlags = NIIF_USER;
    //  Shell_NotifyIcon(NIM_MODIFY, &notifyIcon);
    //}
  }

  DisplayStStatus(hWndStickyTweets, status);
  return status;
}

StickyTweetsStatus RefreshSentDirectComplete(RequestContext* pRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  EmptyRects(directUI);

  int newSentDms = UpdateTimelineUI(pRequest->OutputXml, directUI);

  if (newSentDms > 0)
  {
    if (stPage == DirectPage)
    {
      RedrawCurrentPage();
    }
  }

  DisplayStStatus(hWndStickyTweets, status);
  return status;
}

StickyTweetsStatus RefreshUserTimeline(CString screenName)
{
  ATLTRACE2(L"RefreshUserTimeline(%s)\n", screenName);

  CString responseXml;
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  if (screenName != userTimelineLink.Text)
  {
    userUI.RemoveAll();
    userTimelineLink.Text = screenName;
    userTimelineLink.Rect = RectF();

    if (app.IsFollowing(screenName))
      SendMessage(hWndFollow, BM_SETCHECK, (WPARAM)BST_CHECKED, 0);
    else
      SendMessage(hWndFollow, BM_SETCHECK, (WPARAM)BST_UNCHECKED, 0);

    if (stPage == UserTimeline)
    {
      RedrawCurrentPage();
    }
  }

  StickyTweetsStatus error = app.GetUserTimelineXml(screenName, responseXml);

  DisplayStStatus(hWndStickyTweets, error);
  return error;
}

StickyTweetsStatus RefreshUserTimelineComplete(RequestContext* pRequest)
{
  StickyTweetsStatus status(StatusStOk);

  BOOL reQuery = pRequest->Query == userTimelineLink.Text;

  if (reQuery)
    EmptyRects(userUI);
  else
    userUI.RemoveAll();

  int newTweets = UpdateTimelineUI(pRequest->OutputXml, userUI);

  userTimelineLink.Text = pRequest->Query;
  userTimelineLink.Rect = RectF();
  searchLink.Rect = RectF();
  status = StatusStNewUserTimeline;

  if (!reQuery || newTweets > 0)
  {
    scrollPositions[UserTimeline] = 0;
    if (stPage == UserTimeline)
    {
      RedrawCurrentPage();
    }
  }

  DisplayStStatus(hWndStickyTweets, status);

  return status;
}

StickyTweetsStatus RefreshSearch(CString query)
{
  ATLTRACE2(L"RefreshSearch(%s)\n", query);
  CString responseXml;
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus error = app.GetSearchAtom(query, responseXml);

  DisplayStStatus(hWndStickyTweets, error);
  return error;
}

StickyTweetsStatus RefreshSearchComplete(RequestContext* pRequest)
{
  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  StickyTweetsStatus status(StatusStOk);

  BOOL reQuery = pRequest->Query == searchLink.Text;

  if (reQuery)
    EmptyRects(searchUI);
  else
    searchUI.RemoveAll();

  int newTweets = UpdateTimelineUI(pRequest->OutputXml, searchUI, AtomXml);
  searchLink.Text = pRequest->Query;
  searchLink.Rect = RectF();
  userTimelineLink.Rect = RectF();
  status = StatusStNewSearch;

  if (app.IsSearchFilter(pRequest->Query))
    SendMessage(hWndFilter, BM_SETCHECK, (WPARAM)BST_CHECKED, 0);
  else
    SendMessage(hWndFilter, BM_SETCHECK, (WPARAM)BST_UNCHECKED, 0);

  if (!reQuery || newTweets > 0)
  {
    scrollPositions[SearchPage] = 0;

    if (stPage == SearchPage)
    {
      ATLTRACE2(L"Redraw search\n");
      RedrawCurrentPage();
    }
  }

  DisplayStStatus(hWndStickyTweets, status);
  return status;
}

VOID ResizeTweetsChild()
{
  RECT clientRect;
  GetClientRect(hWndStickyTweets, &clientRect);
  INT height, y;
  switch (stPage)
  {
  case UserTimeline:
    if (!userIconRect.IsEmptyArea())
    {
      y = static_cast<INT>(userIconRect.GetBottom()) + 8;
      height = clientRect.bottom - y;
      MoveWindow(hWndTweetsChild, TweetsChildXNormal, y, clientRect.right - TweetsChildXNormal, height, TRUE);
    }
    else
    {
      y = TweetsChildYUserNormal;
      height = clientRect.bottom - y;
      MoveWindow(hWndTweetsChild, TweetsChildXNormal, y, clientRect.right - TweetsChildXNormal, height, TRUE);
    }
    break;

  case SearchPage:
    y = TweetsChildYUserNormal;
    height = clientRect.bottom - y;
    MoveWindow(hWndTweetsChild, TweetsChildXNormal, y, clientRect.right - TweetsChildXNormal, height, TRUE);
    break;

  default:
    height = clientRect.bottom - TweetsChildYNormal;
    MoveWindow(hWndTweetsChild, TweetsChildXNormal, TweetsChildYNormal, clientRect.right - TweetsChildXNormal, height, TRUE);
    break;
  }

  scrollData.Valid = false;
  UpdateScrollData();
}

VOID ScrollBy(INT delta)
{
  ScrollTo(scrollPositions[stPage] + delta);
}

VOID ScrollTo(INT pos)
{
  pos = min(pos, scrollData.NumLines - scrollData.LinesPerPage);
  pos = max(pos, 0);

  RECT updateRect;
  INT scrollReturn = ScrollWindowEx(hWndTweetsChild, 0, (scrollData.ScrollBarPos - pos) * scrollData.LineHeight,
                 NULL, NULL, NULL, &updateRect, SW_ERASE | SW_INVALIDATE);

  scrollData.ScrollBarPos = pos;
  scrollPositions[stPage] = pos;

  SCROLLINFO si;
  si.cbSize = sizeof(si);
  si.fMask = SIF_PAGE | SIF_POS | SIF_RANGE;
  si.nPage = scrollData.LinesPerPage;
  si.nMin = 0;
  si.nMax = scrollData.NumLines - 1;
  si.nPos = scrollData.ScrollBarPos;
  SetScrollInfo(hWndTweetsChild, SB_VERT, &si, TRUE);
}

VOID Search()
{
  TCHAR query[MaxTwitterStatus];
  INT queryLen = GetWindowText(hWndSearchEdit, query, MaxTwitterStatus);
  if (queryLen > 0)
  {
    if (RefreshSearch(query) == StatusStNewSearch || stPage != SearchPage)
      ShowSearchPage();
    SetWindowText(hWndSearchEdit, L"");
  }
}

VOID ShowCurrentTimeline(bool redraw)
{
  bool changePage = stPage != CurrentTimeline;
  if (!changePage && !requireRefresh[CurrentTimeline] && !redraw)
    return;

  ShowWindow(hWndFollow, SW_HIDE);
  ShowWindow(hWndFilter, SW_HIDE);

  ATLTRACE2(L"ShowCurrentTimeline\n");
  stPage = CurrentTimeline;
  EmptyRects(homeUI);
  pCurrentTweets = &homeUI;

  if (requireRefresh[CurrentTimeline])
  {
    requireRefresh[CurrentTimeline] = FALSE;
    RefreshCurrentTimeline();
  }

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  ResizeTweetsChild();
  if (changePage)
  {
    ScrollTo(scrollPositions[stPage]);
  }

  UpdateStatusControls();
  InvalidateRect(hWndStickyTweets, NULL, TRUE);
}

VOID ShowDirectPage(bool redraw)
{
  bool changePage = stPage != DirectPage;
  if (!changePage && !requireRefresh[DirectPage] && !redraw)
    return;

  ShowWindow(hWndFollow, SW_HIDE);
  ShowWindow(hWndFilter, SW_HIDE);

  ATLTRACE2(L"ShowDirectPage\n");
  stPage = DirectPage;
  EmptyRects(directUI);
  pCurrentTweets = &directUI;

  if (requireRefresh[DirectPage])
  {
    requireRefresh[DirectPage] = FALSE;
    RefreshDirectPage();
  }

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  ResizeTweetsChild();
  if (changePage)
  {
    ScrollTo(scrollPositions[stPage]);
  }

  UpdateStatusControls();
  InvalidateRect(hWndStickyTweets, NULL, TRUE);
}

VOID ShowUserTimeline(bool redraw)
{
  bool changePage = stPage != UserTimeline;
  if (!changePage && !requireRefresh[UserTimeline] && !redraw)
    return;

  ShowWindow(hWndFilter, SW_HIDE);

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
  if (userTimelineLink.Text == app.GetSettings().ScreenName)
    ShowWindow(hWndFollow, SW_HIDE);
  else
    ShowWindow(hWndFollow, SW_SHOW);

  ATLTRACE2(L"ShowUserTimeline\n");
  stPage = UserTimeline;
  pCurrentTweets = &userUI;

  if (requireRefresh[UserTimeline])
  {
    requireRefresh[UserTimeline] = FALSE;
    RefreshUserTimeline(userTimelineLink.Text);
  }

  userIconRect = RectF();

  RECT followRect;
  GetClientRect(hWndFollow, &followRect);
  MoveWindow(hWndFollow, FollowButtonXNormal, FollowButtonYNormal, followRect.right - followRect.left, followRect.bottom - followRect.top, TRUE);

  ResizeTweetsChild();
  ScrollTo(scrollPositions[stPage]);

  UpdateStatusControls();
  InvalidateRect(hWndStickyTweets, NULL, TRUE);
}

VOID ShowSearchPage(bool redraw)
{
  bool changePage = stPage != SearchPage;
  if (!changePage && !requireRefresh[SearchPage] && !redraw)
    return;

  ShowWindow(hWndFollow, SW_HIDE);
  ShowWindow(hWndFilter, SW_SHOW);

  ATLTRACE2(L"ShowSearchPage\n");
  stPage = SearchPage;
  pCurrentTweets = &searchUI;

  if (requireRefresh[SearchPage])
  {
    requireRefresh[SearchPage] = FALSE;
    RefreshSearch(searchLink.Text);
  }

  ResizeTweetsChild();
  ScrollTo(scrollPositions[stPage]);

  UpdateStatusControls();
  InvalidateRect(hWndStickyTweets, NULL, TRUE);
}

VOID ShowStickyTweets()
{
  if (GetForegroundWindow() != hWndStickyTweets)
  {
    if (!IsWindowVisible(hWndStickyTweets))
      ShowWindow(hWndStickyTweets, IsIconic(hWndStickyTweets) ? SW_RESTORE : SW_SHOW);
    SetForegroundWindow(hWndStickyTweets);
  }
}

BOOL UpdateLastViewed(BOOL invalidateCheck)
{
  newTweetTotal = 0;
  if (homeUI.GetCount() > 0)
  {
    TwitterStatus& topStatus = homeUI.GetHead();
    if (topStatus.Id.Compare(lastViewedId) != 0)
    {
      ATLTRACE2(L"UpdateLastViewed: From %s to %s\n", lastViewedId, topStatus.Id);
      lastViewedId = topStatus.Id;

      if (invalidateCheck && IsWindowVisible(hWndStickyTweets) && !IsIconic(hWndStickyTweets))
        InvalidateRect(hWndStickyTweets, NULL, TRUE);

      return TRUE;
    }
  }
  return FALSE;
}

void UpdateScrollData()
{
  if (!scrollData.Valid)
  {
    scrollData.LineHeight = static_cast<INT>(LineHeight);
    scrollData.NumLines = 0;

    POSITION e = pCurrentTweets->GetHeadPosition();

    while (e)
    {
      TwitterStatus& twitterStatus = pCurrentTweets->GetNext(e);
      if (twitterStatus.HasLayout)
        scrollData.NumLines += twitterStatus.NumLines + 2;
      else
        scrollData.NumLines += 3;
    }

    //ATLTRACE2(L"UpdateScrollData scrollData = { LineHeight: %d, NumLines: %d }\n", scrollData.LineHeight, scrollData.NumLines);
    
    if (scrollData.NumLines > 0)
    {
      RECT clientRect;
      GetClientRect(hWndTweetsChild, &clientRect);
      scrollData.LinesPerPage = clientRect.bottom / scrollData.LineHeight;
      //ATLTRACE2(L"                              { LinesPerPage: %d }\n", scrollData.LinesPerPage);
      ScrollBy(0);
      ShowWindow(hWndTweetsChild, SW_SHOW);
    }
    else
    {
      scrollData.LinesPerPage = 0;
      ShowWindow(hWndTweetsChild, SW_HIDE);
    }

    scrollData.Valid = true;
  }
}

VOID UpdateStatusControls(bool clearError)
{
  ATLTRACE2(L"UpdateStatusControls(%s)\n", clearError ? L"true" : L"false");
  // Character counter
  TCHAR status[MaxTwitterStatus];
  INT statusLength = GetWindowText(hWndStatus, status, MaxTwitterStatus);
  swprintf_s<MaxTwitterStatus>(status, L"%d", 140 - statusLength);
  SetWindowText(hWndCharCounter, status);

  if (statusLength == 0)
  {
    if (clearError)
    {
      SetWindowText(hWndError, L"");
    }

    ShowWindow(hWndCancel, SW_HIDE);
    if (inReplyToId.GetLength() > 0)
    {
      inReplyToId = L"";
      InvalidateRect(hWndTweetsChild, NULL, TRUE);
    }
  }
  else
  {
    CString recipient = GetRecipient();
    if (recipient.GetLength() > 0)
    {
      switch (stPage)
      {
      case DirectPage:
        SetWindowText(hWndError, CString(L"Direct Message ") + recipient);
        break;

      default:
        SetWindowText(hWndError, CString(L"Reply to ") + recipient);
        break;
      }
    }

    ShowWindow(hWndCancel, SW_SHOW);
  }
}

VOID UpdateStatus(HWND hWnd)
{
  StickyTweetsStatus error(StatusStOk);
  TCHAR status[MaxTwitterStatus];

  StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();

  if (GetWindowText(hWndStatus, status, MaxTwitterStatus) == 0)
    error = ErrorStEmptyStatus;

  if (error == StatusStOk)
  {
    CString statusStr(status);
    if (stPage == DirectPage)
    {
      CString recipient = GetRecipient();
      if (recipient.GetLength() == 0)
      {
        error = ErrorStDmUser;
      }
      else
      {
        CString dmText = statusStr.Mid(recipient.GetLength() + 2);
        error = app.SendDm(recipient, dmText);
      }
    }
    else
    {
      error = app.UpdateStatus(statusStr, inReplyToId);
    }
  }

  if (error < WarningFirst)
  {
    SetWindowText(hWndStatus, L"");
  }

  DisplayStStatus(hWnd, error);
}

int UpdateTimelineUI(const CString& timelineXml, CAtlList<TwitterStatus>& timelineUI, XmlSchema xmlSchema)
{
  size_t originalSize = timelineUI.GetCount();
  size_t updatedSize = originalSize;

  if (timelineXml.GetLength() > 0)
  {
    CComPtr<ISAXXMLReader> pXmlReader;

    HRESULT hr = CoCreateInstance(__uuidof(SAXXMLReader30), NULL, CLSCTX_ALL, __uuidof(ISAXXMLReader), (void**)&pXmlReader);

    if (FAILED(hr))
    {
      ATLTRACE2(L"Unable to create XML parser\n");
    }
    else
    {
      TwitterContentHandler twitterHandler(timelineUI);
      AtomContentHandler atomHandler(timelineUI);

      switch (xmlSchema)
      {
      case TwitterXml:
        hr = pXmlReader->putContentHandler(&twitterHandler);
        break;

      case AtomXml:
        hr = pXmlReader->putContentHandler(&atomHandler);
        break;
      }

      if (FAILED(hr))
      {
        ATLTRACE2(L"Error setting XML content handler\n");
      }
      else
      {
        CComVariant xmlVar(timelineXml);
        pXmlReader->parse(xmlVar);
      }

      if (stPage != SearchPage)
        FilterTimeline(timelineUI);

      updatedSize = timelineUI.GetCount();

      StickyTweetsAppXP& app = StickyTweetsAppXP::GetInstance();
      DWORD homeMax = app.GetSettings().HomeMax;

      while (timelineUI.GetCount() > homeMax)
        timelineUI.RemoveTail();

      PruneImageMap();

      if (!app.GetSettings().NoImages)
      {
        POSITION e = timelineUI.GetHeadPosition();

        while (e)
        {
          TwitterStatus& twitterStatus = timelineUI.GetNext(e);

          if (imageMap.Lookup(twitterStatus.User.ProfileImageUrl.Text) == NULL)
          {
            Image* pImage(NULL);
            if (app.DownloadImage(twitterStatus.User.ProfileImageUrl.Text, &pImage) == StatusStOk)
              imageMap.SetAt(twitterStatus.User.ProfileImageUrl.Text, pImage);
          }
        }

#ifdef _DEBUG
        POSITION imageE = imageMap.GetStartPosition();
        unsigned totalBytes = 0;
        unsigned totalImages = 0;
        while (imageE)
        {
          Image* image = imageMap.GetNextValue(imageE);
          totalImages++;
          unsigned numPixels = image->GetWidth() * image->GetHeight();
          unsigned bpp = 24;
          if (image->GetPixelFormat() != PixelFormat24bppRGB)
            bpp = 32;
          unsigned numBytes = numPixels * bpp / 8;
          totalBytes += numBytes;
        }
        ATLTRACE2(L"%d bytes in %d stored images\n", totalBytes, totalImages);
#endif
      }
    }
  }

#ifdef _DEBUG
  ATLTRACE2(L"UpdateTimelineUI\n");
  if (&timelineUI == &homeUI)
    ATLTRACE2(L"Home list has %d tweets\n", homeUI.GetCount());
  else if (&timelineUI == &userUI)
    ATLTRACE2(L"User list has %d tweets\n", userUI.GetCount());
  else if (&timelineUI == &searchUI)
    ATLTRACE2(L"Search list has %d tweets\n", searchUI.GetCount());
#endif

  return updatedSize - originalSize;
}
